Last active
July 18, 2018 15:05
-
-
Save desinas/8f369e840e37db8ebcc650743599218c to your computer and use it in GitHub Desktop.
part of Contacts app after bootstrapping w/ Create React app of Udacity React nano-degree
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import { Route } from 'react-router-dom' | |
import ListContacts from './ListContacts' | |
import CreateContact from './CreateContact' | |
import * as ContactsAPI from './utils/ContactsAPI' | |
class App extends Component { | |
state = { | |
contacts: [] | |
} | |
componentDidMount() { | |
ContactsAPI.getAll().then((contacts) => { | |
this.setState({ contacts }) | |
}) | |
} | |
removeContact = (contact) => { | |
this.setState((state) => ({ | |
contacts: state.contacts.filter((c) => c.id !== contact.id) | |
})) | |
ContactsAPI.remove(contact) | |
} | |
createContact(contact) { | |
ContactsAPI.create(contact).then(contact => { | |
this.setState(state => ({ | |
contacts: state.contacts.concat([ contact ]) | |
})) | |
}) | |
} | |
render() { | |
return ( | |
<div> | |
<Route exact path='/' render={() => ( | |
<ListContacts | |
onDeleteContact={this.removeContact} | |
contacts={this.state.contacts} | |
/> | |
)}/> | |
<Route path='/create' render={({ history }) => ( | |
<CreateContact | |
onCreateContact={(contact) => { | |
this.createContact(contact) | |
history.push('/') | |
}} | |
/> | |
)}/> | |
</div> | |
) | |
} | |
} | |
export default App;; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import { Link } from 'react-router-dom' | |
import ImageInput from './ImageInput' | |
import serializeForm from 'form-serialize' | |
class CreateContact extends Component { | |
handleSubmit = (e) => { | |
e.preventDefault() | |
const values = serializeForm(e.target, { hash: true }) | |
if (this.props.onCreateContact) | |
this.props.onCreateContact(values) | |
} | |
render() { | |
return ( | |
<div> | |
<Link className='close-create-contact' to='/'>Close</Link> | |
<form onSubmit={this.handleSubmit} className='create-contact-form'> | |
<ImageInput | |
className='create-contact-avatar-input' | |
name='avatarURL' | |
maxHeight={64} | |
/> | |
<div className='create-contact-details'> | |
<input type='text' name='name' placeholder='Name'/> | |
<input type='text' name='email' placeholder='Email'/> | |
<button>Add Contact</button> | |
</div> | |
</form> | |
</div> | |
) | |
} | |
} | |
export default CreateContact |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import PropTypes from 'prop-types' | |
const readFileAsDataURL = (file) => | |
new Promise(resolve => { | |
const reader = new FileReader() | |
reader.onload = (event) => { | |
resolve(event.target.result) | |
} | |
reader.readAsDataURL(file) | |
}) | |
const resizeImage = (imageURL, canvas, maxHeight) => | |
new Promise(resolve => { | |
const image = new Image() | |
image.onload = () => { | |
const context = canvas.getContext('2d') | |
if (image.height > maxHeight) { | |
image.width *= maxHeight / image.height | |
image.height = maxHeight | |
} | |
context.clearRect(0, 0, canvas.width, canvas.height) | |
canvas.width = image.width | |
canvas.height = image.height | |
context.drawImage(image, 0, 0, image.width, image.height) | |
resolve(canvas.toDataURL('image/jpeg')) | |
} | |
image.src = imageURL | |
}) | |
/** | |
* A custom <input> that dynamically reads and resizes image files before | |
* submitting them to the server as data URLs. Also, shows a preview of the image. | |
*/ | |
class ImageInput extends React.Component { | |
static propTypes = { | |
className: PropTypes.string, | |
name: PropTypes.string, | |
maxHeight: PropTypes.number | |
} | |
state = { | |
value: '' | |
} | |
handleFileChange = (event) => { | |
const file = event.target.files[0] | |
if (file && file.type.match(/^image\//)) { | |
readFileAsDataURL(file).then(originalURL => { | |
resizeImage(originalURL, this.canvas, this.props.maxHeight).then(url => { | |
this.setState({ value: url }) | |
}) | |
}) | |
} else { | |
this.setState({ value: '' }) | |
} | |
} | |
handleFormReset = () => { | |
this.setState({ value: '' }) | |
} | |
componentDidMount() { | |
this.canvas = document.createElement('canvas') | |
this.fileInput.form.addEventListener('reset', this.handleFormReset) | |
} | |
componentWillUnmount() { | |
this.fileInput.form.removeEventListener('reset', this.handleFormReset) | |
} | |
render() { | |
const { className, name } = this.props | |
const { value } = this.state | |
const style = { | |
position: 'relative' | |
} | |
if (value) { | |
style.backgroundImage = `url("${value}")` | |
style.backgroundRepeat = 'no-repeat' | |
style.backgroundPosition = 'center' | |
style.backgroundSize = 'cover' | |
} | |
return ( | |
<div className={className} style={style}> | |
<input type="hidden" name={name} value={value} /> | |
<input | |
ref={node => this.fileInput = node} | |
type="file" | |
onChange={this.handleFileChange} | |
style={{ | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
width: '100%', | |
height: '100%', | |
opacity: 0 | |
}} | |
/> | |
</div> | |
) | |
} | |
} | |
export default ImageInput |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { BrowserRouter } from 'react-router-dom' | |
import App from './App'; | |
import registerServiceWorker from './registerServiceWorker'; | |
import './index.css'; | |
ReactDOM.render( | |
<BrowserRouter><App /></BrowserRouter>, | |
document.getElementById('root') | |
); | |
registerServiceWorker(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import { Link } from 'react-router-dom' | |
import PropTypes from 'prop-types' | |
import escapeRegExp from 'escape-string-regexp' | |
import sortBy from 'sort-by' | |
class ListContacts extends Component { | |
static propTypes = { | |
contacts: PropTypes.array.isRequired, | |
onDeleteContact: PropTypes.func.isRequired | |
} | |
state = { | |
query: '' | |
} | |
updateQuery = (query) => { | |
this.setState({ query: query.trim() }) | |
} | |
clearQuery = () => { | |
this.setState({ query: '' }) | |
} | |
render() { | |
const { contacts, onDeleteContact } = this.props | |
const { query } = this.state | |
let showingContacts | |
if (query) { | |
const match = new RegExp(escapeRegExp(query), 'i') | |
showingContacts = contacts.filter((contact) => match.test(contact.name)) | |
} else { | |
showingContacts = contacts | |
} | |
showingContacts.sort(sortBy('name')) | |
return ( | |
<div className='list-contacts'> | |
<div className='list-contacts-top'> | |
<input | |
className='search-contacts' | |
type='text' | |
placeholder='Search contacts' | |
value={query} | |
onChange={(event) => this.updateQuery(event.target.value)} | |
/> | |
<Link | |
to='/create' | |
className='add-contact' | |
>Add Contact</Link> | |
</div> | |
{showingContacts.length !== contacts.length && ( | |
<div className='showing-contacts'> | |
<span>Now showing {showingContacts.length} of {contacts.length} total</span> | |
<button onClick={this.clearQuery}>Show all</button> | |
</div> | |
)} | |
<ol className='contact-list'> | |
{showingContacts.map((contact) => ( | |
<li key={contact.id} className='contact-list-item'> | |
<div className='contact-avatar' style={{ | |
backgroundImage: `url(${contact.avatarURL})` | |
}}/> | |
<div className='contact-details'> | |
<p>{contact.name}</p> | |
<p>{contact.email}</p> | |
</div> | |
<button onClick={() => onDeleteContact(contact)} className='contact-remove'> | |
Remove | |
</button> | |
</li> | |
))} | |
</ol> | |
</div> | |
) | |
} | |
} | |
export default ListContacts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const api = process.env.REACT_APP_CONTACTS_API_URL || 'http://localhost:5001' | |
let token = localStorage.token | |
if (!token) | |
token = localStorage.token = Math.random().toString(36).substr(-8) | |
const headers = { | |
'Accept': 'application/json', | |
'Authorization': token | |
} | |
export const getAll = () => | |
fetch(`${api}/contacts`, { headers }) | |
.then(res => res.json()) | |
.then(data => data.contacts) | |
export const remove = (contact) => | |
fetch(`${api}/contacts/${contact.id}`, { method: 'DELETE', headers }) | |
.then(res => res.json()) | |
.then(data => data.contact) | |
export const create = (body) => | |
fetch(`${api}/contacts`, { | |
method: 'POST', | |
headers: { | |
...headers, | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify(body) | |
}).then(res => res.json()) |
Repo commits made on every Gist revision
- Render default contacts
- Display contact avatar, name, email, and remove button
- Move default contacts into App component as state
- Hook up delete button to remove contacts
Repo commits
- Add PropTypes package to verify ListContacts props: To use PropTypes in our app, need to install prop-types:
npm install --save prop-types
- Convert ListContacts to a controlled component with a search field
- Filter and sort the contacts that are searched for
- Use destructuring to clean up code
- Adds 'Now showing' section with link to clear the search query
- Use componentDidMount to fetch contacts from the server: Instead of hard coding into the state of app component make an API request to fetch data from the server and add data to the state.
Repo commit - revision #11
- Remove contact from database when deleted in the app
Repo commit - revision #12
- Add CreateContact component that's displayed based on state When state in App.js changes from list to create then the component in CreateContact.js will replace the list in the page. CreateContact component will be in charge of the form to create new contacts. This as a standalone component and used composition by adding it to the render() method in the App component.
In an attempt to do an extremely simple recreation of how React Router works, added a screen property to this.state, and used this property to control what content should display on the screen. If this.state.screen is list then we'll show the list of all existing contacts. If this.state.screen is create then we'll show the CreateContact component.
Repo commit - revision #13
- Add button to create contacts
Repo commit - revision #14
- Wrap App in BrowserRouter component: To use React Router in our app, we need to install react-router-dom.
npm install --save react-router-dom
This wrap sets up the router to be able to work with all the other existing components or going to import next. Also, this wrap listens to URL changes and notifies the other components. BrowserRouter is rendering a Router component and passing it a history prop. History comes from the history library. The whole purpose of this library is it abstracts away the differences in various environments and provides a minimal API that lets you manage the history stack, navigate, confirm navigation, and persist state between sessions.
So in a nutshell, when you use BrowserRouter, you're creating a history object which will listen to changes in the URL and make sure your app is made aware of those changes.
Repo commit - revision #15
- Turn create contact link into a Link component: Link is a straightforward way to provide declarative, accessible navigation around your application. By passing a to property to the Link component, you tell your app which path to route to
<Link to="/about">About</Link>
Changing html anchor tag to Link component of React Router by changing tag-name, href attribute into to attribute and deleting onClick attribute.
Repo commit - revision #16
- Use Routes to control page content: With a Route component if wanted to be able to passed props to a specific component that the router is going to render, will need to use Route's render prop. The Route component is a critical piece of building an application with React Router because it's the component which is going to decide which components are rendered based on the current URL path.
💡 Using of the <Routeexact
/> property makes this route to be rendered only if the address match exactly.
Repo commit - revision #17
- Build form to create new contacts: Form for uploading a profile-picture at new contact and resizing it. Forms for input name and email of new contact form. In order this functionality to work the ImageInput.js file in src folder is needed.
Repo commit - revision #18
- Serialize new contact fields and pass to parent component: Finishing The Contact Form - Handle Submission Of The Contact Form. So, instead of having the browser take over this form when it gets submit, do it with JS. Creating a handler that takes an event, with
e.prevent.default()
the browser no longer going to submit this form and then serialize this with JSserializeForm(e.target, {hash: true})
and thenthis.props.onCreateContact(values)
In order to serialize the form data we'll use the form-serialize package to output this information as a regular JavaScript object for the app to usenpm install --save form-serialize
Repo commit - revision #19
- Save new contact to the server: So there is a contact form. Serialized data and passed it up to the parent component. The last to do to have a fully functional app is to save the contact to the server. To do that some changing must be done at the component in file App.js. At the Route component with
path="/create"
changing the component to render, so that to make a composition of components to render together. In addition passed history in the render we can use it tohistory.push('/')
so that to make the back key of the browser to work properly. Also in render we have onCreateContact prop where we call createContact( contact ) method.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is part of the repo https://github.com/udacity/reactnd-contacts-complete that is a code-along with the first project in the Udacity React Nanodegree program. Most of the commits in this repository correspond to videos in the program.
This project was bootstrapped with Create React App.
Code-along project for the Contacts app https://www.udacity.com/course/react-nanodegree--nd019