diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2019-04-28 15:54:41 +0200 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2019-04-28 15:54:41 +0200 |
| commit | aa5638a1c31ce56d59696580f33733dcf0d7764c (patch) | |
| tree | c2d1bd0e480b9bae113ce9af706927c5b7c55952 | |
| parent | a72ecc91db39ac5a2d60aefc6d767da457500dde (diff) | |
refactor frontend, add threshold slider
| -rw-r--r-- | check/app/server/api.py | 2 | ||||
| -rw-r--r-- | client/actions.js | 27 | ||||
| -rw-r--r-- | client/app.js | 159 | ||||
| -rw-r--r-- | client/components/index.js | 11 | ||||
| -rw-r--r-- | client/components/query.component.js | 103 | ||||
| -rw-r--r-- | client/components/results.component.js | 56 | ||||
| -rw-r--r-- | client/components/timing.component.js | 13 | ||||
| -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.js | 7 | ||||
| -rw-r--r-- | client/store.js | 61 | ||||
| -rw-r--r-- | client/types.js | 16 | ||||
| -rw-r--r-- | package-lock.json | 198 | ||||
| -rw-r--r-- | package.json | 7 |
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", |
