summaryrefslogtreecommitdiff
path: root/client
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 /client
parenta72ecc91db39ac5a2d60aefc6d767da457500dde (diff)
refactor frontend, add threshold slider
Diffstat (limited to 'client')
-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
11 files changed, 303 insertions, 150 deletions
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'