commit
6d8273b795
|
@ -22,12 +22,21 @@ module.exports = {
|
||||||
try {
|
try {
|
||||||
const id = req.param('id')
|
const id = req.param('id')
|
||||||
const value = req.param('url')
|
const value = req.param('url')
|
||||||
|
const author = req.param('author')
|
||||||
|
const publisher = req.param('publisher')
|
||||||
|
const title = req.param('title')
|
||||||
|
const isbn = req.param('isbn')
|
||||||
if (value.length) {
|
if (value.length) {
|
||||||
const url = await TargetUrl.update({ id, user: req.user.id }, { url: value }).fetch()
|
const url = await TargetUrl.update({ id, user: req.user.id }, {
|
||||||
|
url: value,
|
||||||
|
author,
|
||||||
|
publisher,
|
||||||
|
title,
|
||||||
|
isbn
|
||||||
|
}).fetch()
|
||||||
return res.json(url)
|
return res.json(url)
|
||||||
} else {
|
} else {
|
||||||
await TargetUrl.destroyOne({ id })
|
return new HttpError(400, 'URL cannot be blank.').send(res)
|
||||||
return res.status(204).send()
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return (new HttpError(500, e.message)).send(res)
|
return (new HttpError(500, e.message)).send(res)
|
||||||
|
|
|
@ -11,6 +11,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
url: {
|
url: {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
}
|
},
|
||||||
|
author: 'string',
|
||||||
|
publisher: 'string',
|
||||||
|
title: 'string',
|
||||||
|
isbn: 'string'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ const ACTIONS = {
|
||||||
edit_url: 'edit_url',
|
edit_url: 'edit_url',
|
||||||
delete_url: 'delete_url',
|
delete_url: 'delete_url',
|
||||||
list_url: 'list_url',
|
list_url: 'list_url',
|
||||||
|
set_editing: 'set_editing',
|
||||||
error: 'error'
|
error: 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,10 +29,16 @@ export const addUrl = url => ({
|
||||||
data: url
|
data: url
|
||||||
})
|
})
|
||||||
|
|
||||||
export const changeUrlField = (id, value) => ({
|
export const setEditing = id => ({
|
||||||
|
type: ACTIONS.set_editing,
|
||||||
|
data: id
|
||||||
|
})
|
||||||
|
|
||||||
|
export const changeUrlField = (id, what, value) => ({
|
||||||
type: ACTIONS.edit_url,
|
type: ACTIONS.edit_url,
|
||||||
data: {
|
data: {
|
||||||
id,
|
id,
|
||||||
|
what,
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -90,13 +97,14 @@ export const createNewUrl = () => async (dispatch, getState) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setUrl = (id, value) => async (dispatch, getState) => {
|
export const setUrl = (value) => async (dispatch, getState) => {
|
||||||
dispatch(setWorking(true))
|
dispatch(setWorking(true))
|
||||||
try {
|
try {
|
||||||
await Ajax.patch({
|
await Ajax.patch({
|
||||||
url: '/api/targets/' + id,
|
url: '/api/targets/' + value.id,
|
||||||
data: {
|
data: {
|
||||||
url: value
|
...value,
|
||||||
|
id: undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -4,25 +4,96 @@ import React from 'react'
|
||||||
import IconButton from '../components/IconButton'
|
import IconButton from '../components/IconButton'
|
||||||
import UnderlineInput from '../components/UnderlineInput'
|
import UnderlineInput from '../components/UnderlineInput'
|
||||||
import '../../styles/shared/urilistitem.scss'
|
import '../../styles/shared/urilistitem.scss'
|
||||||
import { changeUrlField, setUrl, removeUrl } from '../actions/targets'
|
import { changeUrlField, setUrl, removeUrl, setEditing } from '../actions/targets'
|
||||||
|
|
||||||
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||||
|
const isbnRegex = /^(97(8|9))?\d{9}(\d|X)$/
|
||||||
|
|
||||||
class UriListItem extends React.Component {
|
class UriListItem extends React.Component {
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
this.getView = this.getView.bind(this)
|
||||||
|
this.getEditing = this.getEditing.bind(this)
|
||||||
|
this.cancelEvent = this.cancelEvent.bind(this)
|
||||||
|
}
|
||||||
|
cancelEvent (e, id) {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (id === false) return
|
||||||
|
this.props.dispatch(setEditing(id))
|
||||||
|
}
|
||||||
|
getView () {
|
||||||
|
return (
|
||||||
|
<li className='uri-list-item flex-container' onClick={(e) => this.cancelEvent(e, this.props.item.id)}>
|
||||||
|
<div className='stack flex flex-container flex-vertical'>
|
||||||
|
<span className='label'>Destination URL</span>
|
||||||
|
<span className='value'>{this.props.item.url}</span>
|
||||||
|
</div>
|
||||||
|
<div className='stack flex flex-container flex-vertical'>
|
||||||
|
<span className='label'>Filters</span>
|
||||||
|
<span className='value'>{['publisher', 'title', 'author', 'isbn'].reduce((a, x) => a + (this.props.item[x] ? 1 : 0), 0) || 'None'}</span>
|
||||||
|
</div>
|
||||||
|
<IconButton icon='delete' onClick={() => this.props.dispatch(removeUrl(this.props.item.id))} />
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getEditing () {
|
||||||
|
return (
|
||||||
|
<li className='uri-list-item flex-container flex-vertical editing' onClick={(e) => this.cancelEvent(e, false)}>
|
||||||
|
<header className='flex-container' onClick={(e) => this.cancelEvent(e, null)}>
|
||||||
|
<h3 className='flex'>Editing: {this.props.item.url}</h3>
|
||||||
|
<IconButton icon='delete' onClick={() => this.props.dispatch(removeUrl(this.props.item.id))} />
|
||||||
|
</header>
|
||||||
|
<div className='settings'>
|
||||||
|
<UnderlineInput
|
||||||
|
className='uri flex'
|
||||||
|
type='text'
|
||||||
|
name={'url-' + this.props.item.id}
|
||||||
|
placeholder='Destination URL'
|
||||||
|
value={'' + this.props.item.url}
|
||||||
|
pattern={uriRegex}
|
||||||
|
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'url', e.target.value))}
|
||||||
|
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
|
||||||
|
<h4>Filters</h4>
|
||||||
|
<UnderlineInput
|
||||||
|
className='uri flex'
|
||||||
|
type='text'
|
||||||
|
name={'title-' + this.props.id}
|
||||||
|
placeholder='Ebook title'
|
||||||
|
value={'' + this.props.item.title}
|
||||||
|
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'title', e.target.value))}
|
||||||
|
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
|
||||||
|
<UnderlineInput
|
||||||
|
className='uri flex'
|
||||||
|
type='text'
|
||||||
|
name={'author-' + this.props.item.id}
|
||||||
|
placeholder='Author'
|
||||||
|
value={'' + this.props.item.author}
|
||||||
|
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'author', e.target.value))}
|
||||||
|
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
|
||||||
|
<UnderlineInput
|
||||||
|
className='uri flex'
|
||||||
|
type='text'
|
||||||
|
name={'publisher-' + this.props.item.id}
|
||||||
|
placeholder='Publisher URL'
|
||||||
|
value={'' + this.props.item.publisher}
|
||||||
|
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'publisher', e.target.value))}
|
||||||
|
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
|
||||||
|
<UnderlineInput
|
||||||
|
className='uri flex'
|
||||||
|
type='number'
|
||||||
|
name={'isbn-' + this.props.item.id}
|
||||||
|
placeholder='ISBN'
|
||||||
|
value={'' + this.props.item.isbn}
|
||||||
|
pattern={isbnRegex}
|
||||||
|
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'isbn', e.target.value))}
|
||||||
|
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<li className='uri-list-item flex-container'>
|
this.props.editing ? this.getEditing() : this.getView()
|
||||||
<UnderlineInput
|
|
||||||
className='uri flex'
|
|
||||||
type='text'
|
|
||||||
name={'url-' + this.props.id}
|
|
||||||
placeholder='Destination URL'
|
|
||||||
value={'' + this.props.url}
|
|
||||||
pattern={uriRegex}
|
|
||||||
onChange={(e) => this.props.dispatch(changeUrlField(this.props.id, e.target.value))}
|
|
||||||
onBlur={(e) => this.props.dispatch(setUrl(this.props.id, e.target.value))} />
|
|
||||||
<IconButton icon='delete' onClick={() => this.props.dispatch(removeUrl(this.props.id))} />
|
|
||||||
</li>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,14 @@ const reducer = (state = {}, action) => {
|
||||||
}
|
}
|
||||||
case Actions.edit_url:
|
case Actions.edit_url:
|
||||||
urls = state.urls
|
urls = state.urls
|
||||||
urls.find(x => x.id === data.id).url = data.value
|
urls.find(x => x.id === data.id)[data.what] = data.value
|
||||||
return {
|
return {
|
||||||
urls: urls
|
urls: urls
|
||||||
}
|
}
|
||||||
|
case Actions.set_editing:
|
||||||
|
return {
|
||||||
|
editingUrl: data
|
||||||
|
}
|
||||||
case Actions.error:
|
case Actions.error:
|
||||||
return {
|
return {
|
||||||
error: data.message
|
error: data.message
|
||||||
|
|
|
@ -5,7 +5,7 @@ import ReactDOM from 'react-dom'
|
||||||
import Progress from './components/Progress'
|
import Progress from './components/Progress'
|
||||||
import UriListItem from './containers/UriListItem'
|
import UriListItem from './containers/UriListItem'
|
||||||
import reducer from './reducers/targets'
|
import reducer from './reducers/targets'
|
||||||
import { fetchUrls, createNewUrl } from './actions/targets'
|
import { fetchUrls, createNewUrl, setEditing } from './actions/targets'
|
||||||
import '../styles/targets.scss'
|
import '../styles/targets.scss'
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
|
@ -17,10 +17,8 @@ class App extends React.Component {
|
||||||
email: '',
|
email: '',
|
||||||
password: ''
|
password: ''
|
||||||
},
|
},
|
||||||
urls: [{
|
urls: [],
|
||||||
id: 1,
|
editingUrl: null,
|
||||||
url: 'http'
|
|
||||||
}],
|
|
||||||
working: false
|
working: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,13 +45,13 @@ class App extends React.Component {
|
||||||
return (<UriListItem
|
return (<UriListItem
|
||||||
key={i}
|
key={i}
|
||||||
dispatch={this.dispatch}
|
dispatch={this.dispatch}
|
||||||
id={item.id}
|
item={item}
|
||||||
url={item.url} />)
|
editing={this.state.editingUrl === item.id} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className='root-container flex-container'>
|
<div className='root-container flex-container' onClick={() => this.dispatch(setEditing(null))}>
|
||||||
<aside className='nav nav-left'>
|
<aside className='nav nav-left'>
|
||||||
<header>
|
<header>
|
||||||
<h1>RoE</h1>
|
<h1>RoE</h1>
|
||||||
|
|
|
@ -58,6 +58,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:focus + .reacts-to,
|
&:focus + .reacts-to,
|
||||||
|
&:active + .reacts-to {
|
||||||
|
.underline:before {
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus + .reacts-to,
|
||||||
&:active + .reacts-to,
|
&:active + .reacts-to,
|
||||||
&.has-content + .reacts-to {
|
&.has-content + .reacts-to {
|
||||||
label {
|
label {
|
||||||
|
@ -66,10 +73,6 @@
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
color: $accent-2;
|
color: $accent-2;
|
||||||
}
|
}
|
||||||
.underline:before {
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.invalid {
|
&.invalid {
|
||||||
color: $red;
|
color: $red;
|
||||||
|
|
|
@ -1,12 +1,58 @@
|
||||||
@import '../lib/vars';
|
@import '../lib/vars';
|
||||||
|
|
||||||
.uri-list-item {
|
.uri-list-item {
|
||||||
height: 60px;
|
min-height: 60px;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
background: $background-1;
|
background: white;
|
||||||
box-shadow: $shadow-0;
|
box-shadow: $shadow-0;
|
||||||
|
padding: 0 0 0 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: margin 0.15s $transition,
|
||||||
|
padding 0.15s $transition,
|
||||||
|
border-radius 0.15s $transition;
|
||||||
|
|
||||||
& > .button.icon {
|
.button.icon {
|
||||||
margin: 7px 5px 3px 5px;
|
margin: 7px 5px 3px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.editing {
|
||||||
|
margin: 24px 8px;
|
||||||
|
line-height: initial;
|
||||||
|
padding: 10px 0 10px 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
header {
|
||||||
|
h3 {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.settings {
|
||||||
|
padding: 0 14px 0 0;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 30px 0 0 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
line-height: 25px;
|
||||||
|
padding: 5px 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
span.label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: $black-2;
|
||||||
|
}
|
||||||
|
span.value {
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
margin: -14px 0 8px 0;
|
margin: -14px 0 8px 0;
|
||||||
}
|
}
|
||||||
header {
|
& > header {
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -37,10 +37,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.list {
|
.list {
|
||||||
margin: 14px;
|
margin: 20px 14px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: hidden;
|
// overflow: hidden;
|
||||||
}
|
}
|
||||||
&.working {
|
&.working {
|
||||||
& > .progress {
|
& > .progress {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
exports.up = function (knex, Promise) {
|
||||||
|
return Promise.all([
|
||||||
|
knex.schema.table('targeturl', t => {
|
||||||
|
t.string('title')
|
||||||
|
t.string('author')
|
||||||
|
t.string('publisher')
|
||||||
|
t.string('isbn')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (knex, Promise) {
|
||||||
|
return Promise.all([
|
||||||
|
knex.schema.table('targeturl', t => {
|
||||||
|
t.dropColumns('title', 'author', 'publisher', 'isbn')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
Loading…
Reference in New Issue