Skip to content

Commit

Permalink
Use Unsplash API (#301)
Browse files Browse the repository at this point in the history
* Add unsplash-js, isomorphic-fetch

* /unsplash route/handling

* RandomImage use unsplash api

* Implement Photographer accredidation

* Add referral link

* Fetch photographer profile_url from unsplash api

* Change credit copy

* Add PhotoCredit component
  • Loading branch information
jakedex authored and Michael Fix committed Apr 8, 2018
1 parent eef096b commit cf1e92b
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 44 deletions.
11 changes: 9 additions & 2 deletions components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,15 @@ class Editor extends React.Component {
this.updateSetting('language', language.mime || language.mode)
}

updateBackground(changes, cb) {
this.setState(changes, cb)
updateBackground({ photographer, ...changes }) {
if (photographer) {
this.setState(({ code = DEFAULT_CODE }) => ({
...changes,
code: code + `\n\n// Photo by ${photographer.name} on Unsplash`
}))
} else {
this.setState(changes)
}
}

render() {
Expand Down
14 changes: 11 additions & 3 deletions components/ImagePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import ReactCrop, { makeAspectCrop } from 'react-image-crop'

import RandomImage from './RandomImage'
import PhotoCredit from './PhotoCredit'
import { fileToDataURL } from '../lib/util'

const getCroppedImg = (imageDataURL, pixelCrop) => {
Expand Down Expand Up @@ -31,7 +32,7 @@ const getCroppedImg = (imageDataURL, pixelCrop) => {
})
}

const INITIAL_STATE = { crop: null, imageAspectRatio: null, pixelCrop: null }
const INITIAL_STATE = { crop: null, imageAspectRatio: null, pixelCrop: null, photographer: null }

export default class extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -88,11 +89,17 @@ export default class extends React.Component {
})
}

selectImage(e) {
selectImage(e, { photographer }) {
const file = e.target ? e.target.files[0] : e

return fileToDataURL(file).then(dataURL =>
this.props.onChange({ backgroundImage: dataURL, backgroundImageSelection: null })
this.setState({ photographer }, () => {
this.props.onChange({
backgroundImage: dataURL,
backgroundImageSelection: null,
photographer
})
})
)
}

Expand Down Expand Up @@ -173,6 +180,7 @@ export default class extends React.Component {
minWidth={10}
keepSelection
/>
{this.state.photographer && <PhotoCredit photographer={this.state.photographer} />}
</div>
<style jsx>
{`
Expand Down
27 changes: 27 additions & 0 deletions components/PhotoCredit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react'

export default ({ photographer }) => (
<div className="photo-credit">
Photo by{' '}
<a href={`${photographer.profile_url}?utm_source=carbon&utm_medium=referral`}>
{photographer.name}
</a>
<style jsx>
{`
.photo-credit {
cursor: unset;
user-select: none;
text-align: left;
font-size: 10px;
color: #aaa;
margin-bottom: -2px;
}
.photo-credit a {
cursor: pointer;
text-decoration: underline;
}
`}
</style>
</div>
)
65 changes: 33 additions & 32 deletions components/RandomImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,59 @@ import React from 'react'
import axios from 'axios'
import Spinner from 'react-spinner'

import { range, fileToDataURL } from '../lib/util'

const RAND_RANGE = 1000000
const UPDATE_SIZE = 20
const WALLPAPER_COLLECTION_ID = 136026
const RANDOM_WALLPAPER_URL = `https://source.unsplash.com/collection/${WALLPAPER_COLLECTION_ID}/240x320`
import PhotoCredit from './PhotoCredit'
import { fileToDataURL } from '../lib/util'

const downloadThumbnailImage = img => {
return axios
.get(img.url, { responseType: 'blob' })
.then(res => res.data)
.then(fileToDataURL)
.then(dataURL => Object.assign(img, { dataURL }))
}

const largerImage = url => url.replace(/w=\d+/, 'w=1920').replace(/&h=\d+/, '')
const getImageDownloadUrl = img =>
axios.get(`/unsplash/download/${img.id}`).then(res => res.data.url)

export default class RandomImage extends React.Component {
class RandomImage extends React.Component {
constructor(props) {
super(props)
this.state = { cacheIndex: 0, loading: false }
this.selectImage = this.selectImage.bind(this)
this.updateCache = this.updateCache.bind(this)
this.getImage = this.getImage.bind(this)
this.getImages = this.getImages.bind(this)
this.nextImage = this.nextImage.bind(this)
}

cache = []
imageUrls = {}

// fetch images in browser (we require window.FileReader)
componentDidMount() {
// clear cache when remounted
this.cache = []
this.updateCache()
}

async getImage() {
// circumvent browser caching
const sig = Math.floor(Math.random() * RAND_RANGE)

const res = await axios.get(`${RANDOM_WALLPAPER_URL}?sig=${sig}`, { responseType: 'blob' })

// image already in cache?
if (this.imageUrls[res.request.responseURL]) return undefined

this.imageUrls[res.request.responseURL] = true
return {
url: res.request.responseURL,
dataURL: await fileToDataURL(res.data)
}
async getImages() {
const imageUrls = await axios.get('/unsplash/random')
return Promise.all(imageUrls.data.map(downloadThumbnailImage))
}

cache = []
imageUrls = {}

selectImage() {
const image = this.cache[this.state.cacheIndex]

this.setState({ loading: true })
axios
.get(largerImage(this.cache[this.state.cacheIndex].url), { responseType: 'blob' })
getImageDownloadUrl(image)
.then(url => axios.get(url, { responseType: 'blob' }))
.then(res => res.data)
.then(this.props.onChange)
.then(blob => this.props.onChange(blob, image))
.then(() => this.setState({ loading: false }))
}

updateCache() {
this.setState({ loading: true })
Promise.all(range(UPDATE_SIZE).map(this.getImage))
.then(imgs => imgs.filter(img => img)) // remove null
this.getImages()
.then(imgs => (this.cache = this.cache.concat(imgs)))
.then(() => this.setState({ loading: false }))
}
Expand All @@ -75,6 +70,8 @@ export default class RandomImage extends React.Component {
}

render() {
const photographer =
this.cache[this.state.cacheIndex] && this.cache[this.state.cacheIndex].photographer
const bgImage = this.cache[this.state.cacheIndex] && this.cache[this.state.cacheIndex].dataURL

return (
Expand All @@ -84,14 +81,16 @@ export default class RandomImage extends React.Component {
<span onClick={this.nextImage}>Try Another</span>
</div>
<div className="image">{this.state.loading && <Spinner />}</div>
{photographer && <PhotoCredit photographer={photographer} />}
<style jsx>
{`
.image {
width: 100%;
height: 120px;
height: 140px;
background: url(${bgImage});
background-size: cover;
background-repeat: no-repeat;
margin-bottom: 4px;
}
.controls {
Expand All @@ -111,3 +110,5 @@ export default class RandomImage extends React.Component {
)
}
}

export default RandomImage
40 changes: 40 additions & 0 deletions handlers/unsplash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require('isomorphic-fetch')
const { default: Unsplash, toJson } = require('unsplash-js')

const WALLPAPER_COLLECTION_ID = 136026

const client = new Unsplash({
applicationId: process.env.UNSPLASH_ACCESS_KEY,
secret: process.env.UNSPLASH_SECRET_KEY,
callbackUrl: process.env.UNSPLASH_CALLBACK_URL
})

const parseImageResult = img => ({
id: img.id,
photographer: {
name: img.user.name,
profile_url: img.user.links.html
},
url: img.urls.small
})

const getRandomImages = () =>
client.photos
.getRandomPhoto({
collections: [WALLPAPER_COLLECTION_ID],
count: 20
})
.then(toJson)
.then(imgs => imgs.map(parseImageResult))

const downloadImage = imageId =>
client.photos
.getPhoto(imageId)
.then(toJson)
.then(client.photos.downloadPhoto)
.then(toJson)

module.exports = {
randomImages: (req, res) => getRandomImages().then(imgs => res.json(imgs)),
downloadImage: (req, res) => downloadImage(req.params.imageId).then(url => res.json(url))
}
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"graphql": "^0.11.7",
"highlight.js": "^9.12.0",
"history": "^4.7.2",
"isomorphic-fetch": "^2.2.1",
"lodash.debounce": "^4.0.8",
"match-sorter": "^2.2.0",
"morgan": "^1.8.2",
Expand All @@ -53,7 +54,8 @@
"react-syntax-highlight": "^15.3.1",
"resize-observer-polyfill": "^1.5.0",
"tohash": "^1.0.2",
"twitter": "^1.7.1"
"twitter": "^1.7.1",
"unsplash-js": "^4.8.0"
},
"devDependencies": {
"@zeit/next-css": "^0.1.5",
Expand All @@ -72,7 +74,10 @@
"TWITTER_CONSUMER_SECRET": "@twitter-consumer-secret",
"TWITTER_ACCESS_TOKEN_KEY": "@twitter-access-token-key",
"TWITTER_ACCESS_TOKEN_SECRET": "@twitter-access-token-secret",
"LOGS_SECRET_PREFIX": "@logs_secret_prefix"
"LOGS_SECRET_PREFIX": "@logs_secret_prefix",
"UNSPLASH_SECRET_KEY": "@unsplash_secret_key",
"UNSPLASH_ACCESS_KEY": "@unsplash_access_key",
"UNSPLASH_CALLBACK_URL": "@unsplash_callback_url"
}
},
"lint-staged": {
Expand Down
11 changes: 7 additions & 4 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,16 @@ app
// set up
const server = express()
const imageHandler = require('./handlers/image')(browser)
const unsplashHandler = require('./handlers/unsplash')

server.use(morgan('tiny'))

// api endpoints
server.post('/twitter', bodyParser.json({ limit: '5mb' }), require('./handlers/twitter'))
server.post('/image', bodyParser.json({ limit: '5mb' }), wrap(imageHandler))
server.get('/unsplash/random', wrap(unsplashHandler.randomImages))
server.get('/unsplash/download/:imageId', wrap(unsplashHandler.downloadImage))

server.get('/about', (req, res) => app.render(req, res, '/about'))

// if root, render webpage from next
Expand All @@ -47,10 +54,6 @@ app
// otherwise, try and get gist
server.get('*', handle)

// api endpoints
server.post('/twitter', bodyParser.json({ limit: '5mb' }), require('./handlers/twitter'))
server.post('/image', bodyParser.json({ limit: '5mb' }), wrap(imageHandler))

server.listen(port, '0.0.0.0', err => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
Expand Down
34 changes: 33 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2655,6 +2655,10 @@ form-data@~2.1.1:
combined-stream "^1.0.5"
mime-types "^2.1.12"

form-urlencoded@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/form-urlencoded/-/form-urlencoded-1.2.0.tgz#16ce2cafa76d2e48b9e513ab723228aea5993396"

forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
Expand Down Expand Up @@ -3532,7 +3536,7 @@ isobject@^3.0.0, isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"

isomorphic-fetch@^2.1.1:
isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies:
Expand Down Expand Up @@ -3868,6 +3872,10 @@ lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"

lodash.get@4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"

lodash.isarguments@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
Expand Down Expand Up @@ -5204,6 +5212,10 @@ querystring@0.2.0, querystring@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"

querystringify@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb"

ramda@0.24.1:
version "0.24.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857"
Expand Down Expand Up @@ -5583,6 +5595,10 @@ require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"

requires-port@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"

resize-observer-polyfill@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69"
Expand Down Expand Up @@ -6488,6 +6504,15 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"

unsplash-js@^4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/unsplash-js/-/unsplash-js-4.8.0.tgz#8a5a8ccbdf39410ffb9fdd5f04ed2651ea644349"
dependencies:
form-urlencoded "1.2.0"
lodash.get "4.4.2"
querystring "0.2.0"
url-parse "1.2.0"

unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
Expand Down Expand Up @@ -6526,6 +6551,13 @@ url-parse-lax@^1.0.0:
dependencies:
prepend-http "^1.0.1"

url-parse@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986"
dependencies:
querystringify "~1.0.0"
requires-port "~1.0.0"

url@0.11.0, url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
Expand Down

0 comments on commit cf1e92b

Please sign in to comment.