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.POST("/create", h.handleCreate)
protected.POST("/lookup", h.handleLookup)
protected.POST("/recent", h.handleRecent)
protected.GET("/recent", h.handleRecent)
protected.POST("/visitors", h.handleGetVisitors)
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 MediaQuery from 'react-responsive';
import 'react-datepicker/dist/react-datepicker.css';
import toastr from 'toastr'
import util from '../util/util'
import CustomCard from '../Card/Card'
import './Home.css'
@ -15,34 +15,13 @@ export default class HomeComponent extends Component {
handleCustomExpirationChange = expire => this.setState({ expiration: expire })
handleCustomIDChange = (e, { value }) => {
this.customID = value
fetch("/api/v1/protected/lookup", {
method: "POST",
body: JSON.stringify({
ID: value
}),
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
util.lookupEntry(value, () => this.setState({ showCustomIDError: true }), () => this.setState({ showCustomIDError: false }))
}
})
.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 })
onSettingsChange = (e, { value }) => this.setState({ usedSettings: value })
state = {
links: [],
options: [
{ text: 'Custom URL', value: 'custom' },
{ text: 'Expiration', value: 'expire' },
{ text: 'Password', value: 'protected' }
],
setOptions: [],
usedSettings: [],
showCustomIDError: false,
expiration: null
}
@ -51,34 +30,29 @@ export default class HomeComponent extends Component {
}
handleURLSubmit = () => {
if (!this.state.showCustomIDError) {
fetch('/api/v1/protected/create', {
method: 'POST',
body: JSON.stringify({
util.createEntry({
URL: this.url,
ID: this.customID,
Expiration: this.state.setOptions.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined,
Password: this.state.setOptions.includes("protected") && this.password ? this.password : undefined
}),
headers: {
'Authorization': window.localStorage.getItem('token'),
'Content-Type': 'application/json'
}
})
.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
]]
Expiration: this.state.usedSettings.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined,
Password: this.state.usedSettings.includes("protected") && this.password ? this.password : undefined
}, r => this.setState({
links: [...this.state.links, {
shortenedURL: r.URL,
originalURL: this.url,
expiration: this.state.usedSettings.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined,
deletionURL: 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() {
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 (
<div>
<Segment raised>
@ -99,12 +73,12 @@ export default class HomeComponent extends Component {
</Form.Field>
</MediaQuery>
<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' />
</Form.Field>}
</Form.Group>
<Form.Group widths="equal">
{setOptions.includes("expire") && <Form.Field>
{usedSettings.includes("expire") && <Form.Field>
<DatePicker showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
@ -115,13 +89,13 @@ export default class HomeComponent extends Component {
customInput={<Input label="Expiration" />}
minDate={moment()} />
</Form.Field>}
{setOptions.includes("protected") && <Form.Field>
{usedSettings.includes("protected") && <Form.Field>
<Input type="password" label='Password' onChange={this.handlePasswordChange} /></Form.Field>}
</Form.Group>
</Form>
</Segment>
<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>
</div >
)

18
static/src/Lookup/Lookup.js

@ -1,7 +1,7 @@
import React, { Component } from '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'
export default class LookupComponent extends Component {
@ -11,18 +11,7 @@ export default class LookupComponent extends Component {
handleURLChange = (e, { value }) => this.url = value
handleURLSubmit = () => {
let id = this.url.replace(window.location.origin + "/", "")
fetch("/api/v1/protected/lookup", {
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({
util.lookupEntry(id, res => this.setState({
links: [...this.state.links, [
res.URL,
this.url,
@ -32,7 +21,6 @@ export default class LookupComponent extends Component {
res.Expiration
]]
}))
.catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not fetch lookup: ${error.error}`)) : null)
}
render() {
const { links } = this.state
@ -42,7 +30,7 @@ export default class LookupComponent extends Component {
<Header size='huge'>URL Lookup</Header>
<Form onSubmit={this.handleURLSubmit}>
<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>
</Segment>

19
static/src/Recent/Recent.js

@ -1,27 +1,18 @@
import React, { Component } from 'react'
import { Container, Table, Button, Icon } from 'semantic-ui-react'
import toastr from 'toastr'
import Moment from 'react-moment';
import util from '../util/util'
export default class RecentComponent extends Component {
state = {
recent: null
recent: {}
}
componentDidMount() {
this.loadRecentURLs()
}
loadRecentURLs() {
fetch('/api/v1/protected/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)
loadRecentURLs = () => {
util.getRecentURLs(recent => this.setState({ recent }))
}
onRowClick(id) {
@ -29,7 +20,7 @@ export default class RecentComponent extends Component {
}
onEntryDeletion(entry) {
util.deleteEntry(entry.DeletionURL)
util.deleteEntry(entry.DeletionURL, this.loadRecentURLs)
}
render() {
@ -47,7 +38,7 @@ export default class RecentComponent extends Component {
</Table.Row>
</Table.Header>
<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)}><Moment>{recent[key].Public.CreatedOn}</Moment></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 { Container, Table } from 'semantic-ui-react'
import Moment from 'react-moment';
import toastr from 'toastr'
import util from '../util/util'
export default class VisitorComponent extends Component {
state = {
visitors: [],
info: null
id: "",
entry: null,
visitors: []
}
componentWillMount() {
this.setState({ id: this.props.match.params.id })
fetch("/api/v1/protected/lookup", {
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}`)
})
util.lookupEntry(this.props.match.params.id, entry => this.setState({ entry }))
this.reloadVisitors()
this.loop = setInterval(this.reloadVisitors, 1000)
this.reloadInterval = setInterval(this.reloadVisitors, 1000)
}
reloadVisitors = () => {
fetch('/api/v1/protected/visitors', {
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() {
clearInterval(this.reloadInterval)
}
componentWillUnmount() {
clearInterval(this.loop)
reloadVisitors = () => {
util.getVisitors(this.props.match.params.id, visitors => this.setState({ visitors }))
}
// getUTMSource is a function which generates the output for the utm[...] table column
@ -60,11 +35,11 @@ export default class VisitorComponent extends Component {
}
render() {
const { visitors, id, info } = this.state
const { visitors, id, entry } = this.state
return (
<Container >
{info && <p>
Entry with id '{id}' was created at <Moment>{info.CreatedOn}</Moment> and redirects to '{info.URL}'. Currently it has {visitors.length} visits.
{entry && <p>
Entry with id '{id}' was created at <Moment>{entry.CreatedOn}</Moment> and redirects to '{entry.URL}'. Currently it has {visitors.length} visits.
</p>}
<Table celled>
<Table.Header>

26
static/src/index.js

@ -13,9 +13,10 @@ import Lookup from './Lookup/Lookup'
import Recent from './Recent/Recent'
import Visitors from './Visitors/Visitors'
import util from './util/util'
export default class BaseComponent extends Component {
state = {
oAuthOpen: true,
oAuthPopupOpened: true,
userData: {},
authorized: false,
activeItem: "",
@ -25,7 +26,7 @@ export default class BaseComponent extends Component {
handleItemClick = (e, { name }) => this.setState({ activeItem: name })
onOAuthClose = () => {
this.setState({ oAuthOpen: true })
this.setState({ oAuthPopupOpened: true })
}
componentWillMount() {
@ -33,12 +34,11 @@ export default class BaseComponent extends Component {
.then(d => d.json())
.then(info => this.setState({ info }))
.then(() => this.checkAuth())
.catch(e => toastr.error(`Could not fetch info: ${e}`))
.catch(e => util._reportError(e, "info"))
}
checkAuth = () => {
const that = this,
token = window.localStorage.getItem('token');
const token = window.localStorage.getItem('token');
if (token) {
fetch('/api/v1/auth/check', {
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(d => {
that.setState({
.then(d => this.setState({
userData: d,
authorized: true
})
})
}))
.catch(e => {
toastr.error(`Could not fetch check: ${e}`)
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
var wwidth = 400,
wHeight = 500;
var wLeft = (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}`)
this._oAuthPopup = window.open(url, '', `width=${wwidth}, height=${wHeight}, top=${(window.screen.height / 2) - (wHeight / 2)}, left=${(window.screen.width / 2) - (wwidth / 2)}`)
} else {
this._oAuthPopup.location = url;
}
@ -95,10 +91,10 @@ export default class BaseComponent extends Component {
}
render() {
const { oAuthOpen, authorized, activeItem, userData, info } = this.state
const { oAuthPopupOpened, authorized, activeItem, userData, info } = this.state
if (!authorized) {
return (
<Modal size='tiny' open={oAuthOpen} onClose={this.onOAuthClose}>
<Modal size='tiny' open={oAuthPopupOpened} onClose={this.onOAuthClose}>
<Modal.Header>
Authentication
</Modal.Header>

53
static/src/util/util.js

@ -1,10 +1,57 @@
import toastr from 'toastr'
export default class UtilHelper {
static deleteEntry(url) {
static deleteEntry(url, cb) {
fetch(url)
.then(res => res.ok ? res.json() : Promise.reject(res.json()))
.then(() => this.loadRecentURLs())
.catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not delete: ${error.error}`)) : null)
.then(cb())
.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