Use Unsplash API (#301)

* 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
main
Jake Dexheimer 7 years ago committed by Michael Fix
parent eef096bc49
commit cf1e92b8cb

@ -175,8 +175,15 @@ class Editor extends React.Component {
this.updateSetting('language', language.mime || language.mode) this.updateSetting('language', language.mime || language.mode)
} }
updateBackground(changes, cb) { updateBackground({ photographer, ...changes }) {
this.setState(changes, cb) if (photographer) {
this.setState(({ code = DEFAULT_CODE }) => ({
...changes,
code: code + `\n\n// Photo by ${photographer.name} on Unsplash`
}))
} else {
this.setState(changes)
}
} }
render() { render() {

@ -2,6 +2,7 @@ import React from 'react'
import ReactCrop, { makeAspectCrop } from 'react-image-crop' import ReactCrop, { makeAspectCrop } from 'react-image-crop'
import RandomImage from './RandomImage' import RandomImage from './RandomImage'
import PhotoCredit from './PhotoCredit'
import { fileToDataURL } from '../lib/util' import { fileToDataURL } from '../lib/util'
const getCroppedImg = (imageDataURL, pixelCrop) => { const getCroppedImg = (imageDataURL, pixelCrop) => {
@ -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 { export default class extends React.Component {
constructor(props) { constructor(props) {
@ -88,11 +89,17 @@ export default class extends React.Component {
}) })
} }
selectImage(e) { selectImage(e, { photographer }) {
const file = e.target ? e.target.files[0] : e const file = e.target ? e.target.files[0] : e
return fileToDataURL(file).then(dataURL => return fileToDataURL(file).then(dataURL =>
this.props.onChange({ backgroundImage: dataURL, backgroundImageSelection: null }) this.setState({ photographer }, () => {
this.props.onChange({
backgroundImage: dataURL,
backgroundImageSelection: null,
photographer
})
})
) )
} }
@ -173,6 +180,7 @@ export default class extends React.Component {
minWidth={10} minWidth={10}
keepSelection keepSelection
/> />
{this.state.photographer && <PhotoCredit photographer={this.state.photographer} />}
</div> </div>
<style jsx> <style jsx>
{` {`

@ -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>
)

@ -2,25 +2,33 @@ import React from 'react'
import axios from 'axios' import axios from 'axios'
import Spinner from 'react-spinner' import Spinner from 'react-spinner'
import { range, fileToDataURL } from '../lib/util' import PhotoCredit from './PhotoCredit'
import { fileToDataURL } from '../lib/util'
const RAND_RANGE = 1000000 const downloadThumbnailImage = img => {
const UPDATE_SIZE = 20 return axios
const WALLPAPER_COLLECTION_ID = 136026 .get(img.url, { responseType: 'blob' })
const RANDOM_WALLPAPER_URL = `https://source.unsplash.com/collection/${WALLPAPER_COLLECTION_ID}/240x320` .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) { constructor(props) {
super(props) super(props)
this.state = { cacheIndex: 0, loading: false } this.state = { cacheIndex: 0, loading: false }
this.selectImage = this.selectImage.bind(this) this.selectImage = this.selectImage.bind(this)
this.updateCache = this.updateCache.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) this.nextImage = this.nextImage.bind(this)
} }
cache = []
imageUrls = {}
// fetch images in browser (we require window.FileReader) // fetch images in browser (we require window.FileReader)
componentDidMount() { componentDidMount() {
// clear cache when remounted // clear cache when remounted
@ -28,38 +36,25 @@ export default class RandomImage extends React.Component {
this.updateCache() this.updateCache()
} }
async getImage() { async getImages() {
// circumvent browser caching const imageUrls = await axios.get('/unsplash/random')
const sig = Math.floor(Math.random() * RAND_RANGE) return Promise.all(imageUrls.data.map(downloadThumbnailImage))
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)
} }
}
cache = []
imageUrls = {}
selectImage() { selectImage() {
const image = this.cache[this.state.cacheIndex]
this.setState({ loading: true }) this.setState({ loading: true })
axios getImageDownloadUrl(image)
.get(largerImage(this.cache[this.state.cacheIndex].url), { responseType: 'blob' }) .then(url => axios.get(url, { responseType: 'blob' }))
.then(res => res.data) .then(res => res.data)
.then(this.props.onChange) .then(blob => this.props.onChange(blob, image))
.then(() => this.setState({ loading: false })) .then(() => this.setState({ loading: false }))
} }
updateCache() { updateCache() {
this.setState({ loading: true }) this.setState({ loading: true })
Promise.all(range(UPDATE_SIZE).map(this.getImage)) this.getImages()
.then(imgs => imgs.filter(img => img)) // remove null
.then(imgs => (this.cache = this.cache.concat(imgs))) .then(imgs => (this.cache = this.cache.concat(imgs)))
.then(() => this.setState({ loading: false })) .then(() => this.setState({ loading: false }))
} }
@ -75,6 +70,8 @@ export default class RandomImage extends React.Component {
} }
render() { 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 const bgImage = this.cache[this.state.cacheIndex] && this.cache[this.state.cacheIndex].dataURL
return ( return (
@ -84,14 +81,16 @@ export default class RandomImage extends React.Component {
<span onClick={this.nextImage}>Try Another</span> <span onClick={this.nextImage}>Try Another</span>
</div> </div>
<div className="image">{this.state.loading && <Spinner />}</div> <div className="image">{this.state.loading && <Spinner />}</div>
{photographer && <PhotoCredit photographer={photographer} />}
<style jsx> <style jsx>
{` {`
.image { .image {
width: 100%; width: 100%;
height: 120px; height: 140px;
background: url(${bgImage}); background: url(${bgImage});
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
margin-bottom: 4px;
} }
.controls { .controls {
@ -111,3 +110,5 @@ export default class RandomImage extends React.Component {
) )
} }
} }
export default RandomImage

@ -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))
}

@ -31,6 +31,7 @@
"graphql": "^0.11.7", "graphql": "^0.11.7",
"highlight.js": "^9.12.0", "highlight.js": "^9.12.0",
"history": "^4.7.2", "history": "^4.7.2",
"isomorphic-fetch": "^2.2.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"match-sorter": "^2.2.0", "match-sorter": "^2.2.0",
"morgan": "^1.8.2", "morgan": "^1.8.2",
@ -53,7 +54,8 @@
"react-syntax-highlight": "^15.3.1", "react-syntax-highlight": "^15.3.1",
"resize-observer-polyfill": "^1.5.0", "resize-observer-polyfill": "^1.5.0",
"tohash": "^1.0.2", "tohash": "^1.0.2",
"twitter": "^1.7.1" "twitter": "^1.7.1",
"unsplash-js": "^4.8.0"
}, },
"devDependencies": { "devDependencies": {
"@zeit/next-css": "^0.1.5", "@zeit/next-css": "^0.1.5",
@ -72,7 +74,10 @@
"TWITTER_CONSUMER_SECRET": "@twitter-consumer-secret", "TWITTER_CONSUMER_SECRET": "@twitter-consumer-secret",
"TWITTER_ACCESS_TOKEN_KEY": "@twitter-access-token-key", "TWITTER_ACCESS_TOKEN_KEY": "@twitter-access-token-key",
"TWITTER_ACCESS_TOKEN_SECRET": "@twitter-access-token-secret", "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": { "lint-staged": {

@ -36,9 +36,16 @@ app
// set up // set up
const server = express() const server = express()
const imageHandler = require('./handlers/image')(browser) const imageHandler = require('./handlers/image')(browser)
const unsplashHandler = require('./handlers/unsplash')
server.use(morgan('tiny')) 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')) server.get('/about', (req, res) => app.render(req, res, '/about'))
// if root, render webpage from next // if root, render webpage from next
@ -47,10 +54,6 @@ app
// otherwise, try and get gist // otherwise, try and get gist
server.get('*', handle) 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 => { server.listen(port, '0.0.0.0', err => {
if (err) throw err if (err) throw err
console.log(`> Ready on http://localhost:${port}`) console.log(`> Ready on http://localhost:${port}`)

@ -2655,6 +2655,10 @@ form-data@~2.1.1:
combined-stream "^1.0.5" combined-stream "^1.0.5"
mime-types "^2.1.12" 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: forwarded@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -3532,7 +3536,7 @@ isobject@^3.0.0, isobject@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" 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" version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies: dependencies:
@ -3868,6 +3872,10 @@ lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" 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: lodash.isarguments@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@ -5204,6 +5212,10 @@ querystring@0.2.0, querystring@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" 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: ramda@0.24.1:
version "0.24.1" version "0.24.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857"
@ -5583,6 +5595,10 @@ require-main-filename@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" 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: resize-observer-polyfill@^1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69"
@ -6488,6 +6504,15 @@ unset-value@^1.0.0:
has-value "^0.3.1" has-value "^0.3.1"
isobject "^3.0.0" 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: unzip-response@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
@ -6526,6 +6551,13 @@ url-parse-lax@^1.0.0:
dependencies: dependencies:
prepend-http "^1.0.1" 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: url@0.11.0, url@^0.11.0:
version "0.11.0" version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"

Loading…
Cancel
Save