Browse Source

Refactored Frontend (fix #51)

dependabot/npm_and_yarn/web/prismjs-1.21.0
Max Schmitt 8 years ago
parent
commit
2e21c5d413
  1. 2
      handlers/handlers.go
  2. 74
      static/src/Home/Home.js
  3. 18
      static/src/Lookup/Lookup.js
  4. 19
      static/src/Recent/Recent.js
  5. 51
      static/src/Visitors/Visitors.js
  6. 26
      static/src/index.js
  7. 53
      static/src/util/util.js

2
handlers/handlers.go

@ -79,7 +79,7 @@ func (h *Handler) setHandlers() error {
protected.Use(h.authMiddleware) protected.Use(h.authMiddleware)
protected.POST("/create", h.handleCreate) protected.POST("/create", h.handleCreate)
protected.POST("/lookup", h.handleLookup) protected.POST("/lookup", h.handleLookup)
protected.POST("/recent", h.handleRecent) protected.GET("/recent", h.handleRecent)
protected.POST("/visitors", h.handleGetVisitors) protected.POST("/visitors", h.handleGetVisitors)
h.engine.GET("/api/v1/info", h.handleInfo) h.engine.GET("/api/v1/info", h.handleInfo)

74
static/src/Home/Home.js

@ -4,8 +4,8 @@ import DatePicker from 'react-datepicker';
import moment from 'moment'; import moment from 'moment';
import MediaQuery from 'react-responsive'; import MediaQuery from 'react-responsive';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import toastr from 'toastr'
import util from '../util/util'
import CustomCard from '../Card/Card' import CustomCard from '../Card/Card'
import './Home.css' import './Home.css'
@ -15,34 +15,13 @@ export default class HomeComponent extends Component {
handleCustomExpirationChange = expire => this.setState({ expiration: expire }) handleCustomExpirationChange = expire => this.setState({ expiration: expire })
handleCustomIDChange = (e, { value }) => { handleCustomIDChange = (e, { value }) => {
this.customID = value this.customID = value
fetch("/api/v1/protected/lookup", { util.lookupEntry(value, () => this.setState({ showCustomIDError: true }), () => this.setState({ showCustomIDError: false }))
method: "POST",
body: JSON.stringify({
ID: value
}),
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
} }
}) onSettingsChange = (e, { value }) => this.setState({ usedSettings: value })
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(() => {
this.setState({ showCustomIDError: true })
})
.catch(e => {
this.setState({ showCustomIDError: false })
})
}
onSettingsChange = (e, { value }) => this.setState({ setOptions: value })
state = { state = {
links: [], links: [],
options: [ usedSettings: [],
{ text: 'Custom URL', value: 'custom' },
{ text: 'Expiration', value: 'expire' },
{ text: 'Password', value: 'protected' }
],
setOptions: [],
showCustomIDError: false, showCustomIDError: false,
expiration: null expiration: null
} }
@ -51,34 +30,29 @@ export default class HomeComponent extends Component {
} }
handleURLSubmit = () => { handleURLSubmit = () => {
if (!this.state.showCustomIDError) { if (!this.state.showCustomIDError) {
fetch('/api/v1/protected/create', { util.createEntry({
method: 'POST',
body: JSON.stringify({
URL: this.url, URL: this.url,
ID: this.customID, ID: this.customID,
Expiration: this.state.setOptions.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined, Expiration: this.state.usedSettings.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined,
Password: this.state.setOptions.includes("protected") && this.password ? this.password : undefined Password: this.state.usedSettings.includes("protected") && this.password ? this.password : undefined
}), }, r => this.setState({
headers: { links: [...this.state.links, {
'Authorization': window.localStorage.getItem('token'), shortenedURL: r.URL,
'Content-Type': 'application/json' originalURL: this.url,
} expiration: this.state.usedSettings.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined,
}) deletionURL: r.DeletionURL
.then(res => res.ok ? res.json() : Promise.reject(res.json())) }]
.then(r => this.setState({
links: [...this.state.links, [
r.URL,
this.url,
this.state.setOptions.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined,
r.DeletionURL
]]
})) }))
.catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not fetch lookup: ${error.error}`)) : toastr.error(`Could not fetch create: ${e}`))
} }
} }
render() { render() {
const { links, options, setOptions, showCustomIDError, expiration } = this.state const options = [
{ text: 'Custom URL', value: 'custom' },
{ text: 'Expiration', value: 'expire' },
{ text: 'Password', value: 'protected' }
]
const { links, usedSettings, showCustomIDError, expiration } = this.state
return ( return (
<div> <div>
<Segment raised> <Segment raised>
@ -99,12 +73,12 @@ export default class HomeComponent extends Component {
</Form.Field> </Form.Field>
</MediaQuery> </MediaQuery>
<Form.Group className="FieldsMarginButtomFix"> <Form.Group className="FieldsMarginButtomFix">
{setOptions.includes("custom") && <Form.Field error={showCustomIDError} width={16}> {usedSettings.includes("custom") && <Form.Field error={showCustomIDError} width={16}>
<Input label={window.location.origin + "/"} onChange={this.handleCustomIDChange} placeholder='my-shortened-url' /> <Input label={window.location.origin + "/"} onChange={this.handleCustomIDChange} placeholder='my-shortened-url' />
</Form.Field>} </Form.Field>}
</Form.Group> </Form.Group>
<Form.Group widths="equal"> <Form.Group widths="equal">
{setOptions.includes("expire") && <Form.Field> {usedSettings.includes("expire") && <Form.Field>
<DatePicker showTimeSelect <DatePicker showTimeSelect
timeFormat="HH:mm" timeFormat="HH:mm"
timeIntervals={15} timeIntervals={15}
@ -115,13 +89,13 @@ export default class HomeComponent extends Component {
customInput={<Input label="Expiration" />} customInput={<Input label="Expiration" />}
minDate={moment()} /> minDate={moment()} />
</Form.Field>} </Form.Field>}
{setOptions.includes("protected") && <Form.Field> {usedSettings.includes("protected") && <Form.Field>
<Input type="password" label='Password' onChange={this.handlePasswordChange} /></Form.Field>} <Input type="password" label='Password' onChange={this.handlePasswordChange} /></Form.Field>}
</Form.Group> </Form.Group>
</Form> </Form>
</Segment> </Segment>
<Card.Group itemsPerRow="2" stackable style={{ marginTop: "1rem" }}> <Card.Group itemsPerRow="2" stackable style={{ marginTop: "1rem" }}>
{links.map((link, i) => <CustomCard key={i} header={new URL(link[1]).hostname} expireDate={link[2]} metaHeader={link[1]} description={link[0]} deletionURL={link[3]} />)} {links.map((link, i) => <CustomCard key={i} header={new URL(link.originalURL).hostname} expireDate={link.expiration} metaHeader={link.originalURL} description={link.shortenedURL} deletionURL={link.deletionURL} />)}
</Card.Group> </Card.Group>
</div > </div >
) )

18
static/src/Lookup/Lookup.js

@ -1,7 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Segment, Header, Form, Input, Card, Button } from 'semantic-ui-react' import { Segment, Header, Form, Input, Card, Button } from 'semantic-ui-react'
import toastr from 'toastr'
import util from '../util/util'
import CustomCard from '../Card/Card' import CustomCard from '../Card/Card'
export default class LookupComponent extends Component { export default class LookupComponent extends Component {
@ -11,18 +11,7 @@ export default class LookupComponent extends Component {
handleURLChange = (e, { value }) => this.url = value handleURLChange = (e, { value }) => this.url = value
handleURLSubmit = () => { handleURLSubmit = () => {
let id = this.url.replace(window.location.origin + "/", "") let id = this.url.replace(window.location.origin + "/", "")
fetch("/api/v1/protected/lookup", { util.lookupEntry(id, res => this.setState({
method: "POST",
body: JSON.stringify({
ID: id
}),
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(res => this.setState({
links: [...this.state.links, [ links: [...this.state.links, [
res.URL, res.URL,
this.url, this.url,
@ -32,7 +21,6 @@ export default class LookupComponent extends Component {
res.Expiration res.Expiration
]] ]]
})) }))
.catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not fetch lookup: ${error.error}`)) : null)
} }
render() { render() {
const { links } = this.state const { links } = this.state
@ -42,7 +30,7 @@ export default class LookupComponent extends Component {
<Header size='huge'>URL Lookup</Header> <Header size='huge'>URL Lookup</Header>
<Form onSubmit={this.handleURLSubmit}> <Form onSubmit={this.handleURLSubmit}>
<Form.Field> <Form.Field>
<Input required size='big' ref={input => this.urlInput = input} action={{ icon: 'arrow right', labelPosition: 'right', content: 'Lookup' }} type='url' onChange={this.handleURLChange} name='url' placeholder={window.location.origin + "/..."} autoComplete="off"/> <Input required size='big' ref={input => this.urlInput = input} action={{ icon: 'arrow right', labelPosition: 'right', content: 'Lookup' }} type='url' onChange={this.handleURLChange} name='url' placeholder={window.location.origin + "/..."} autoComplete="off" />
</Form.Field> </Form.Field>
</Form> </Form>
</Segment> </Segment>

19
static/src/Recent/Recent.js

@ -1,27 +1,18 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Container, Table, Button, Icon } from 'semantic-ui-react' import { Container, Table, Button, Icon } from 'semantic-ui-react'
import toastr from 'toastr'
import Moment from 'react-moment'; import Moment from 'react-moment';
import util from '../util/util' import util from '../util/util'
export default class RecentComponent extends Component { export default class RecentComponent extends Component {
state = { state = {
recent: null recent: {}
} }
componentDidMount() { componentDidMount() {
this.loadRecentURLs() this.loadRecentURLs()
} }
loadRecentURLs() { loadRecentURLs = () => {
fetch('/api/v1/protected/recent', { util.getRecentURLs(recent => this.setState({ recent }))
method: 'POST',
headers: {
'Authorization': window.localStorage.getItem('token'),
}
})
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(recent => this.setState({ recent: recent }))
.catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could load recent URLs: ${error.error}`)) : null)
} }
onRowClick(id) { onRowClick(id) {
@ -29,7 +20,7 @@ export default class RecentComponent extends Component {
} }
onEntryDeletion(entry) { onEntryDeletion(entry) {
util.deleteEntry(entry.DeletionURL) util.deleteEntry(entry.DeletionURL, this.loadRecentURLs)
} }
render() { render() {
@ -47,7 +38,7 @@ export default class RecentComponent extends Component {
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{recent && Object.keys(recent).map(key => <Table.Row key={key} title="Click to view visitor statistics"> {Object.keys(recent).map(key => <Table.Row key={key} title="Click to view visitor statistics">
<Table.Cell onClick={this.onRowClick.bind(this, key)}>{recent[key].Public.URL}</Table.Cell> <Table.Cell onClick={this.onRowClick.bind(this, key)}>{recent[key].Public.URL}</Table.Cell>
<Table.Cell onClick={this.onRowClick.bind(this, key)}><Moment>{recent[key].Public.CreatedOn}</Moment></Table.Cell> <Table.Cell onClick={this.onRowClick.bind(this, key)}><Moment>{recent[key].Public.CreatedOn}</Moment></Table.Cell>
<Table.Cell>{`${window.location.origin}/${key}`}</Table.Cell> <Table.Cell>{`${window.location.origin}/${key}`}</Table.Cell>

51
static/src/Visitors/Visitors.js

@ -1,53 +1,28 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Container, Table } from 'semantic-ui-react' import { Container, Table } from 'semantic-ui-react'
import Moment from 'react-moment'; import Moment from 'react-moment';
import toastr from 'toastr'
import util from '../util/util'
export default class VisitorComponent extends Component { export default class VisitorComponent extends Component {
state = { state = {
visitors: [], id: "",
info: null entry: null,
visitors: []
} }
componentWillMount() { componentWillMount() {
this.setState({ id: this.props.match.params.id }) this.setState({ id: this.props.match.params.id })
fetch("/api/v1/protected/lookup", { util.lookupEntry(this.props.match.params.id, entry => this.setState({ entry }))
method: "POST",
body: JSON.stringify({
ID: this.props.match.params.id
}),
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(info => this.setState({ info }))
.catch(e => {
toastr.error(`Could not fetch lookup: ${e}`)
})
this.reloadVisitors() this.reloadVisitors()
this.loop = setInterval(this.reloadVisitors, 1000) this.reloadInterval = setInterval(this.reloadVisitors, 1000)
} }
reloadVisitors = () => { componentWillUnmount() {
fetch('/api/v1/protected/visitors', { clearInterval(this.reloadInterval)
method: 'POST',
body: JSON.stringify({
ID: this.props.match.params.id
}),
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(visitors => this.setState({ visitors }))
.catch(e => e.done(res => toastr.error(`Could not fetch visitors: ${res}`)))
} }
componentWillUnmount() { reloadVisitors = () => {
clearInterval(this.loop) util.getVisitors(this.props.match.params.id, visitors => this.setState({ visitors }))
} }
// getUTMSource is a function which generates the output for the utm[...] table column // getUTMSource is a function which generates the output for the utm[...] table column
@ -60,11 +35,11 @@ export default class VisitorComponent extends Component {
} }
render() { render() {
const { visitors, id, info } = this.state const { visitors, id, entry } = this.state
return ( return (
<Container > <Container >
{info && <p> {entry && <p>
Entry with id '{id}' was created at <Moment>{info.CreatedOn}</Moment> and redirects to '{info.URL}'. Currently it has {visitors.length} visits. Entry with id '{id}' was created at <Moment>{entry.CreatedOn}</Moment> and redirects to '{entry.URL}'. Currently it has {visitors.length} visits.
</p>} </p>}
<Table celled> <Table celled>
<Table.Header> <Table.Header>

26
static/src/index.js

@ -13,9 +13,10 @@ import Lookup from './Lookup/Lookup'
import Recent from './Recent/Recent' import Recent from './Recent/Recent'
import Visitors from './Visitors/Visitors' import Visitors from './Visitors/Visitors'
import util from './util/util'
export default class BaseComponent extends Component { export default class BaseComponent extends Component {
state = { state = {
oAuthOpen: true, oAuthPopupOpened: true,
userData: {}, userData: {},
authorized: false, authorized: false,
activeItem: "", activeItem: "",
@ -25,7 +26,7 @@ export default class BaseComponent extends Component {
handleItemClick = (e, { name }) => this.setState({ activeItem: name }) handleItemClick = (e, { name }) => this.setState({ activeItem: name })
onOAuthClose = () => { onOAuthClose = () => {
this.setState({ oAuthOpen: true }) this.setState({ oAuthPopupOpened: true })
} }
componentWillMount() { componentWillMount() {
@ -33,12 +34,11 @@ export default class BaseComponent extends Component {
.then(d => d.json()) .then(d => d.json())
.then(info => this.setState({ info })) .then(info => this.setState({ info }))
.then(() => this.checkAuth()) .then(() => this.checkAuth())
.catch(e => toastr.error(`Could not fetch info: ${e}`)) .catch(e => util._reportError(e, "info"))
} }
checkAuth = () => { checkAuth = () => {
const that = this, const token = window.localStorage.getItem('token');
token = window.localStorage.getItem('token');
if (token) { if (token) {
fetch('/api/v1/auth/check', { fetch('/api/v1/auth/check', {
method: 'POST', method: 'POST',
@ -50,16 +50,14 @@ export default class BaseComponent extends Component {
} }
}) })
.then(res => res.ok ? res.json() : Promise.reject(`incorrect response status code: ${res.status}; text: ${res.statusText}`)) .then(res => res.ok ? res.json() : Promise.reject(`incorrect response status code: ${res.status}; text: ${res.statusText}`))
.then(d => { .then(d => this.setState({
that.setState({
userData: d, userData: d,
authorized: true authorized: true
}) }))
})
.catch(e => { .catch(e => {
toastr.error(`Could not fetch check: ${e}`) toastr.error(`Could not fetch check: ${e}`)
window.localStorage.removeItem('token'); window.localStorage.removeItem('token');
that.setState({ authorized: false }) this.setState({ authorized: false })
}) })
} }
} }
@ -81,9 +79,7 @@ export default class BaseComponent extends Component {
// Open the oAuth window that is it centered in the middle of the screen // Open the oAuth window that is it centered in the middle of the screen
var wwidth = 400, var wwidth = 400,
wHeight = 500; wHeight = 500;
var wLeft = (window.screen.width / 2) - (wwidth / 2); this._oAuthPopup = window.open(url, '', `width=${wwidth}, height=${wHeight}, top=${(window.screen.height / 2) - (wHeight / 2)}, left=${(window.screen.width / 2) - (wwidth / 2)}`)
var wTop = (window.screen.height / 2) - (wHeight / 2);
this._oAuthPopup = window.open(url, '', `width=${wwidth}, height=${wHeight}, top=${wTop}, left=${wLeft}`)
} else { } else {
this._oAuthPopup.location = url; this._oAuthPopup.location = url;
} }
@ -95,10 +91,10 @@ export default class BaseComponent extends Component {
} }
render() { render() {
const { oAuthOpen, authorized, activeItem, userData, info } = this.state const { oAuthPopupOpened, authorized, activeItem, userData, info } = this.state
if (!authorized) { if (!authorized) {
return ( return (
<Modal size='tiny' open={oAuthOpen} onClose={this.onOAuthClose}> <Modal size='tiny' open={oAuthPopupOpened} onClose={this.onOAuthClose}>
<Modal.Header> <Modal.Header>
Authentication Authentication
</Modal.Header> </Modal.Header>

53
static/src/util/util.js

@ -1,10 +1,57 @@
import toastr from 'toastr' import toastr from 'toastr'
export default class UtilHelper { export default class UtilHelper {
static deleteEntry(url) { static deleteEntry(url, cb) {
fetch(url) fetch(url)
.then(res => res.ok ? res.json() : Promise.reject(res.json())) .then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(() => this.loadRecentURLs()) .then(cb())
.catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not delete: ${error.error}`)) : null) .catch(e => this._reportError(e, "delete entry"))
}
static _constructFetch(url, body, cbSucc, cbErr) {
fetch(url, {
method: "POST",
body: JSON.stringify(body),
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(res => cbSucc ? cbSucc(res) : null)
.catch(e => {
if (cbErr) {
cbErr(e)
} else {
let name = url.split("/").pop()
this._reportError(e, name)
}
})
}
static _reportError(e, name) {
if (e instanceof Promise) {
e.then(error => toastr.error(`Could not fetch ${name}: ${error.error}`))
} else {
toastr.error(`Could not fetch ${name}: ${e}`)
}
}
static lookupEntry(ID, cbSucc, cbErr) {
this._constructFetch("/api/v1/protected/lookup", { ID }, cbSucc, cbErr)
}
static getVisitors(ID, cbSucc) {
this._constructFetch("/api/v1/protected/visitors", { ID }, cbSucc)
}
static createEntry(entry, cbSucc) {
this._constructFetch("/api/v1/protected/create",entry, cbSucc)
}
static getRecentURLs(cbSucc) {
fetch('/api/v1/protected/recent', {
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(res => cbSucc ? cbSucc(res) : null)
.catch(e => this._reportError(e, "recent"))
} }
}; };
Loading…
Cancel
Save