summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2019-04-28 15:54:41 +0200
committerJules Laplace <julescarbon@gmail.com>2019-04-28 15:54:41 +0200
commitaa5638a1c31ce56d59696580f33733dcf0d7764c (patch)
treec2d1bd0e480b9bae113ce9af706927c5b7c55952
parenta72ecc91db39ac5a2d60aefc6d767da457500dde (diff)
refactor frontend, add threshold slider
-rw-r--r--check/app/server/api.py2
-rw-r--r--client/actions.js27
-rw-r--r--client/app.js159
-rw-r--r--client/components/index.js11
-rw-r--r--client/components/query.component.js103
-rw-r--r--client/components/results.component.js56
-rw-r--r--client/components/timing.component.js13
-rw-r--r--client/components/upload.helpers.js (renamed from client/lib/upload.helpers.js)0
-rw-r--r--client/components/uploadImage.component.js (renamed from client/lib/uploadImage.component.js)0
-rw-r--r--client/index.js7
-rw-r--r--client/store.js61
-rw-r--r--client/types.js16
-rw-r--r--package-lock.json198
-rw-r--r--package.json7
14 files changed, 501 insertions, 159 deletions
diff --git a/check/app/server/api.py b/check/app/server/api.py
index acaef76..e73e584 100644
--- a/check/app/server/api.py
+++ b/check/app/server/api.py
@@ -115,7 +115,7 @@ def similar():
"""
Search by uploading an image
"""
- params, error = get_params(default_threshold=SIMILARITY_THRESHOLD, default_limit=SIMILARITY_LIMIT)
+ params, error = get_params(default_threshold=SIMILAR_THRESHOLD, default_limit=SIMILAR_LIMIT)
if error:
return jsonify({
'success': False,
diff --git a/client/actions.js b/client/actions.js
new file mode 100644
index 0000000..dfcfa09
--- /dev/null
+++ b/client/actions.js
@@ -0,0 +1,27 @@
+import { get, post } from './util'
+import * as types from './types'
+
+export const api = (dispatch, method, tag, url, params, after) => {
+ dispatch({ type: types.api.loading, tag })
+ return method(url, params).then(data => {
+ if (after) data = after(data)
+ dispatch({ type: types.api.loaded, tag, data })
+ return data
+ }).catch(err => {
+ dispatch({ type: types.api.error, tag, err })
+ })
+}
+
+export const upload = (blob, threshold) => dispatch => {
+ const params = new FormData()
+ params.append('q', blob)
+ params.append('threshold', threshold)
+ api(dispatch, post, 'similar', '/api/v1/similar', params)
+}
+
+export const submit = (url, threshold) => dispatch => {
+ const params = new FormData()
+ params.append('url', url)
+ params.append('threshold', threshold)
+ api(dispatch, post, 'similar', '/api/v1/similar', params)
+}
diff --git a/client/app.js b/client/app.js
index 8a07f34..c9a479c 100644
--- a/client/app.js
+++ b/client/app.js
@@ -1,155 +1,16 @@
import React, { Component } from 'react'
-import UploadImage from './lib/uploadImage.component'
-import { post } from './util'
+import { Query, Results, Timing } from './components'
import './app.css'
-const initialState = {
- image: null,
- url: "",
- res: null,
- loading: false,
+export default function App () {
+ return (
+ <div className='app'>
+ <h1>Search by Image</h1>
+ <Query />
+ <Results />
+ <Timing />
+ </div>
+ )
}
-
-export default class PhashApp extends Component {
- state = { ...initialState }
-
- upload(blob) {
- if (this.state.image) {
- URL.revokeObjectURL(this.state.image)
- }
- const url = URL.createObjectURL(blob)
- this.setState({ image: url, loading: true })
-
- const fd = new FormData()
- fd.append('q', blob)
- post('/api/v1/match', fd)
- .then(res => {
- console.log(res)
- this.setState({ res, loading: false })
- })
- .catch(err => {
- console.log(err)
- this.setState({ loading: false })
- })
- }
-
- submit() {
- const { url } = this.state
- if (!url || url.indexOf('http') !== 0) return
- this.setState({ image: url, loading: true })
-
- const fd = new FormData()
- fd.append('url', url)
- post('/api/v1/match', fd)
- .then(res => {
- console.log(res)
- this.setState({ res, loading: false })
- })
- .catch(err => {
- console.log(err)
- this.setState({ loading: false })
- })
- }
-
- render() {
- const { res } = this.state
- return (
- <div className='app'>
- <h1>Search by Image</h1>
- {this.renderQuery()}
- {this.renderResults()}
- {this.renderTiming()}
- </div>
- )
- }
-
- renderQuery() {
- const { image } = this.state
- const style = {}
- if (image) {
- style.backgroundImage = 'url(' + image + ')'
- style.backgroundSize = 'cover'
- style.opacity = 1
- }
- return (
- <div className='query'>
- <label>
- <span>Query image</span>
- <UploadImage onUpload={this.upload.bind(this)} />
- </label>
- <br />
- <label>
- <span>Add a URL</span>
- <input
- type='text'
- value={this.state.url}
- onChange={e => this.setState({ url: e.target.value })}
- onKeyDown={e => e.keyCode === 13 && this.submit()}
- placeholder='https://'
- />
- </label>
- {image && <div style={style} />}
- </div>
- )
- }
- renderResults() {
- const { loading, res } = this.state
- if (!res) {
- return (
- <div className='results'>
- </div>
- )
- }
- if (loading) {
- return (
- <div className='results'>
- <i>Loading...</i>
- </div>
- )
- }
-
- const { success, error, match, results } = res
- if (!success) {
- return (
- <div className='results'>
- <b>Error: {error.replace(/_/g, ' ')}</b>
- </div>
- )
- }
-
- if (!match) {
- return (
- <div className='results'>
- No match, image added to database
- </div>
- )
- }
-
- return (
- <div className='results'>
- {results.map(({ phash, score, sha256, url }) => (
- <div className='result' key={sha256}>
- <div className='img'><img src={url} /></div>
- <br />
- {score == 0
- ? <span className='score'><b>Exact match</b></span>
- : <span className='score'>Score: {score}</span>
- }<br />
- <span className='sha256'>{sha256}</span>
- Phash: {phash.toString(16)}
- </div>
- ))}
- </div>
- )
- }
-
- renderTiming() {
- const { res } = this.state
- if (res && res.timing) {
- return <small>Query completed in {Math.round(res.timing * 1000)} ms</small>
- }
- return null
- }
-} \ No newline at end of file
diff --git a/client/components/index.js b/client/components/index.js
new file mode 100644
index 0000000..8c3f07a
--- /dev/null
+++ b/client/components/index.js
@@ -0,0 +1,11 @@
+import Query from './query.component'
+import Results from './results.component'
+import Timing from './timing.component'
+import UploadImage from './uploadImage.component'
+
+export {
+ Query,
+ Results,
+ Timing,
+ UploadImage
+} \ No newline at end of file
diff --git a/client/components/query.component.js b/client/components/query.component.js
new file mode 100644
index 0000000..bd98ce8
--- /dev/null
+++ b/client/components/query.component.js
@@ -0,0 +1,103 @@
+import React, { Component } from 'react'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import * as actions from '../actions'
+
+import UploadImage from './uploadImage.component'
+class Query extends Component {
+ state = {
+ image: null,
+ blob: null,
+ url: "",
+ threshold: 20,
+ }
+
+ handleUpload(blob) {
+ const { image, threshold } = this.state
+ if (image) {
+ URL.revokeObjectURL(image)
+ }
+ const url = URL.createObjectURL(blob)
+ this.setState({ image: url, blob })
+ this.props.actions.upload(blob, threshold)
+ }
+
+ handleURL() {
+ const { url, threshold } = this.state
+ if (!url || url.indexOf('http') !== 0) return
+ this.setState({ image: url, blob: null })
+ this.props.actions.submit(url, threshold)
+ }
+
+ resubmit() {
+ const { image, blob } = this.state
+ if (blob) {
+ this.handleUpload(blob)
+ } else {
+ this.handleURL()
+ }
+ }
+
+ render() {
+ const { url, image, threshold } = this.state
+ const style = {}
+ if (image) {
+ style.maxWidth = 200
+ style.maxHeight = 200
+ style.backgroundImage = 'url(' + image + ')'
+ style.backgroundSize = 'cover'
+ style.opacity = 1
+ }
+ return (
+ <div className='query'>
+ <label>
+ <span>Query image</span>
+ <UploadImage onUpload={this.handleUpload.bind(this)} />
+ </label>
+ {/*
+ <label>
+ <span>Add a URL</span>
+ <input
+ type='text'
+ value={url}
+ onChange={e => this.setState({ url: e.target.value })}
+ onKeyDown={e => e.keyCode === 13 && this.handleURL()}
+ placeholder='https://'
+ />
+ </label>
+ */}
+ <label>
+ <span>Threshold</span>
+ <input
+ type='number'
+ value={threshold}
+ min={0}
+ max={64}
+ step={1}
+ onChange={e => this.setState({ threshold: parseInt(e.target.value) }) }
+ />
+ <input
+ type='range'
+ value={threshold}
+ min={0}
+ max={64}
+ step={1}
+ onChange={e => this.setState({ threshold: parseInt(e.target.value) }) }
+ />
+ <button onClick={this.resubmit}>Update</button>
+ </label>
+ {image && <div className='image' style={style} />}
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ ...state.api,
+})
+const mapDispatchToProps = dispatch => ({
+ actions: bindActionCreators({ ...actions }, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(Query)
diff --git a/client/components/results.component.js b/client/components/results.component.js
new file mode 100644
index 0000000..a6f3052
--- /dev/null
+++ b/client/components/results.component.js
@@ -0,0 +1,56 @@
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+
+function Results({ loading, res }) {
+ if (!res) {
+ return (
+ <div className='results'>
+ </div>
+ )
+ }
+ if (loading) {
+ return (
+ <div className='results'>
+ <i>Loading...</i>
+ </div>
+ )
+ }
+
+ const { success, error, match, results } = res
+ if (!success) {
+ return (
+ <div className='results'>
+ <b>Error: {error.replace(/_/g, ' ')}</b>
+ </div>
+ )
+ }
+
+ if (!match) {
+ return (
+ <div className='results'>
+ No match, image added to database
+ </div>
+ )
+ }
+
+ return (
+ <div className='results'>
+ {results.map(({ phash, score, sha256, url }) => (
+ <div className='result' key={sha256}>
+ <div className='img'><img src={url} /></div>
+ <br />
+ {score == 0
+ ? <span className='score'><b>Exact match</b></span>
+ : <span className='score'>Score: {score}</span>
+ }<br />
+ <span className='sha256'>{sha256}</span>
+ Phash: {phash.toString(16)}
+ </div>
+ ))}
+ </div>
+ )
+}
+
+const mapStateToProps = state => state.api
+
+export default connect(mapStateToProps)(Results)
diff --git a/client/components/timing.component.js b/client/components/timing.component.js
new file mode 100644
index 0000000..7473a51
--- /dev/null
+++ b/client/components/timing.component.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import { connect } from 'react-redux'
+
+function Timing({ timing }) {
+ if (timing) {
+ return <small>Query completed in {Math.round(timing * 1000)} ms</small>
+ }
+ return null
+}
+
+const mapStateToProps = state => state.api
+
+export default connect(mapStateToProps)(Timing)
diff --git a/client/lib/upload.helpers.js b/client/components/upload.helpers.js
index d0baab4..d0baab4 100644
--- a/client/lib/upload.helpers.js
+++ b/client/components/upload.helpers.js
diff --git a/client/lib/uploadImage.component.js b/client/components/uploadImage.component.js
index 690c0dc..690c0dc 100644
--- a/client/lib/uploadImage.component.js
+++ b/client/components/uploadImage.component.js
diff --git a/client/index.js b/client/index.js
index 1a71e55..b9d3a1a 100644
--- a/client/index.js
+++ b/client/index.js
@@ -1,8 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom'
+import { Provider } from 'react-redux'
import App from './app'
+import { store, history } from './store'
+
ReactDOM.render(
- <App />, document.querySelector('#container')
+ <Provider store={store}>
+ <App history={history} />
+ </Provider>, document.querySelector('#container')
)
diff --git a/client/store.js b/client/store.js
new file mode 100644
index 0000000..c5f5555
--- /dev/null
+++ b/client/store.js
@@ -0,0 +1,61 @@
+import { applyMiddleware, compose, combineReducers, createStore } from 'redux'
+import { connectRouter, routerMiddleware } from 'connected-react-router'
+import { createBrowserHistory } from 'history'
+import thunk from 'redux-thunk'
+import * as types from './types'
+
+const initialState = () => ({
+ api: null,
+})
+
+export default function apiReducer(state = initialState(), action) {
+ // console.log(action.type, action)
+ switch (action.type) {
+ case types.api.loading:
+ return {
+ ...state,
+ [action.tag]: { loading: true },
+ }
+
+ case types.api.loaded:
+ return {
+ ...state,
+ [action.tag]: action.data,
+ }
+
+ case types.api.error:
+ return {
+ ...state,
+ [action.tag]: { error: action.err },
+ }
+
+ default:
+ return state
+ }
+}
+
+const rootReducer = combineReducers({
+ api: apiReducer,
+})
+
+function configureStore(initialState = {}, history) {
+ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
+
+ const store = createStore(
+ connectRouter(history)(rootReducer), // new root reducer with router state
+ initialState,
+ composeEnhancers(
+ applyMiddleware(
+ thunk,
+ routerMiddleware(history)
+ ),
+ ),
+ )
+
+ return store
+}
+
+const history = createBrowserHistory()
+const store = configureStore({}, history)
+
+export { store, history }
diff --git a/client/types.js b/client/types.js
new file mode 100644
index 0000000..6311816
--- /dev/null
+++ b/client/types.js
@@ -0,0 +1,16 @@
+export const asType = (type, name) => [type, name].join('_').toUpperCase()
+export const tagAsType = (type, names) => (
+ names.reduce((tags, name) => {
+ tags[name] = asType(type, name)
+ return tags
+ }, {})
+)
+
+export const api = tagAsType('api', [
+ 'loading', 'loaded', 'error',
+])
+
+export const system = tagAsType('system', [
+])
+
+export const init = '@@INIT'
diff --git a/package-lock.json b/package-lock.json
index 4a124f6..e71f414 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -368,6 +368,11 @@
"dev": true,
"optional": true
},
+ "asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
+ },
"asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
@@ -1788,6 +1793,16 @@
"typedarray": "^0.0.6"
}
},
+ "connected-react-router": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.4.0.tgz",
+ "integrity": "sha512-RZRLD7qUz9OdmCn0JkW7pOiUsR7v9NtqnYKfqrxXsfO2ozMLR2/MjHaSPpdbMr4VE5TY6MwzAXUSkheN2ldqug==",
+ "requires": {
+ "immutable": "^3.8.1",
+ "prop-types": "^15.7.2",
+ "seamless-immutable": "^7.1.3"
+ }
+ },
"console-browserify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
@@ -1887,6 +1902,15 @@
"sha.js": "^2.4.8"
}
},
+ "create-react-context": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz",
+ "integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==",
+ "requires": {
+ "fbjs": "^0.8.0",
+ "gud": "^1.0.0"
+ }
+ },
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@@ -2238,6 +2262,14 @@
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
},
+ "encoding": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+ "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+ "requires": {
+ "iconv-lite": "~0.4.13"
+ }
+ },
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -3054,6 +3086,27 @@
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
"dev": true
},
+ "fbjs": {
+ "version": "0.8.17",
+ "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
+ "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
+ "requires": {
+ "core-js": "^1.0.0",
+ "isomorphic-fetch": "^2.1.1",
+ "loose-envify": "^1.0.0",
+ "object-assign": "^4.1.0",
+ "promise": "^7.1.1",
+ "setimmediate": "^1.0.5",
+ "ua-parser-js": "^0.7.18"
+ },
+ "dependencies": {
+ "core-js": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
+ "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
+ }
+ }
+ },
"figures": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
@@ -4154,6 +4207,11 @@
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
"dev": true
},
+ "gud": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
+ "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
+ },
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -4270,6 +4328,19 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
+ "history": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/history/-/history-4.9.0.tgz",
+ "integrity": "sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "loose-envify": "^1.2.0",
+ "resolve-pathname": "^2.2.0",
+ "tiny-invariant": "^1.0.2",
+ "tiny-warning": "^1.0.0",
+ "value-equal": "^0.4.0"
+ }
+ },
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -4415,7 +4486,6 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@@ -4461,6 +4531,11 @@
"invariant": "^2.2.0"
}
},
+ "immutable": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+ "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
+ },
"import-local": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",
@@ -4860,8 +4935,7 @@
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
- "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
- "dev": true
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-symbol": {
"version": "1.0.2",
@@ -4900,6 +4974,15 @@
"isarray": "1.0.0"
}
},
+ "isomorphic-fetch": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
+ "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
+ "requires": {
+ "node-fetch": "^1.0.1",
+ "whatwg-fetch": ">=0.10.0"
+ }
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5395,6 +5478,15 @@
"lower-case": "^1.1.1"
}
},
+ "node-fetch": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
+ "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
+ "requires": {
+ "encoding": "^0.1.11",
+ "is-stream": "^1.0.1"
+ }
+ },
"node-libs-browser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz",
@@ -5839,6 +5931,21 @@
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
+ "path-to-regexp": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
+ "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
+ "requires": {
+ "isarray": "0.0.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ }
+ }
+ },
"path-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
@@ -6070,6 +6177,14 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
+ "promise": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "requires": {
+ "asap": "~2.0.3"
+ }
+ },
"promise-inflight": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -6260,6 +6375,23 @@
"react-lifecycles-compat": "^3.0.0"
}
},
+ "react-router": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.0.tgz",
+ "integrity": "sha512-6EQDakGdLG/it2x9EaCt9ZpEEPxnd0OCLBHQ1AcITAAx7nCnyvnzf76jKWG1s2/oJ7SSviUgfWHofdYljFexsA==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "create-react-context": "^0.2.2",
+ "history": "^4.9.0",
+ "hoist-non-react-statics": "^3.1.0",
+ "loose-envify": "^1.3.1",
+ "path-to-regexp": "^1.7.0",
+ "prop-types": "^15.6.2",
+ "react-is": "^16.6.0",
+ "tiny-invariant": "^1.0.2",
+ "tiny-warning": "^1.0.0"
+ }
+ },
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@@ -6620,6 +6752,20 @@
"resolve": "^1.1.6"
}
},
+ "redux": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz",
+ "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "symbol-observable": "^1.2.0"
+ }
+ },
+ "redux-thunk": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
+ "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
+ },
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@@ -6812,6 +6958,11 @@
"integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=",
"dev": true
},
+ "resolve-pathname": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
+ "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
+ },
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@@ -6913,8 +7064,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"scheduler": {
"version": "0.13.6",
@@ -6967,6 +7117,11 @@
}
}
},
+ "seamless-immutable": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz",
+ "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A=="
+ },
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
@@ -7011,8 +7166,7 @@
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
- "dev": true
+ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"sha.js": {
"version": "2.4.11",
@@ -7437,6 +7591,11 @@
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
"dev": true
},
+ "symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+ },
"table": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz",
@@ -7519,6 +7678,16 @@
"setimmediate": "^1.0.4"
}
},
+ "tiny-invariant": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.4.tgz",
+ "integrity": "sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g=="
+ },
+ "tiny-warning": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz",
+ "integrity": "sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q=="
+ },
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -7615,6 +7784,11 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
+ "ua-parser-js": {
+ "version": "0.7.19",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
+ "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
+ },
"uglify-js": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
@@ -7898,6 +8072,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
+ "value-equal": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
+ "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
+ },
"vm-browserify": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
@@ -8682,6 +8861,11 @@
}
}
},
+ "whatwg-fetch": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
+ "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
+ },
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
diff --git a/package.json b/package.json
index 5c4fb15..ef9a8d5 100644
--- a/package.json
+++ b/package.json
@@ -11,15 +11,20 @@
"author": "julescarbon@gmail.com",
"license": "UNLICENSED",
"dependencies": {
+ "connected-react-router": "^6.4.0",
"dotenv": "^7.0.0",
"exifreader": "^2.8.2",
+ "history": "^4.9.0",
"preact": "^8.4.2",
"preact-compat": "^3.18.4",
"prop-types": "^15.6.1",
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-hot-loader": "^4.3.0",
- "react-redux": "^5.0.7"
+ "react-redux": "^5.1.1",
+ "react-router": "^5.0.0",
+ "redux": "^4.0.1",
+ "redux-thunk": "^2.3.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",