Skip to content

Instantly share code, notes, and snippets.

@desinas
Last active July 18, 2018 15:05
Show Gist options
  • Save desinas/8f369e840e37db8ebcc650743599218c to your computer and use it in GitHub Desktop.
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
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;;
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
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
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();
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
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())
@desinas
Copy link
Author

desinas commented Jul 10, 2018

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 <Route exact /> property makes this route to be rendered only if the address match exactly.

@desinas
Copy link
Author

desinas commented Jul 11, 2018

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.

@desinas
Copy link
Author

desinas commented Jul 11, 2018

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 JS serializeForm(e.target, {hash: true}) and then this.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 use npm install --save form-serialize

@desinas
Copy link
Author

desinas commented Jul 11, 2018

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 to history.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