From 2e21c5d413fb3601fde56a633317bb1698b5692d Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 6 Dec 2017 16:23:52 +0100 Subject: [PATCH] Refactored Frontend (fix #51) --- handlers/handlers.go | 2 +- static/src/Home/Home.js | 80 +++++++++++---------------------- static/src/Lookup/Lookup.js | 36 +++++---------- static/src/Recent/Recent.js | 19 +++----- static/src/Visitors/Visitors.js | 51 ++++++--------------- static/src/index.js | 30 ++++++------- static/src/util/util.js | 53 ++++++++++++++++++++-- 7 files changed, 121 insertions(+), 150 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 350724e..47942b5 100644 --- a/handlers/handlers.go +++ b/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) diff --git a/static/src/Home/Home.js b/static/src/Home/Home.js index 5eaf998..3f55325 100644 --- a/static/src/Home/Home.js +++ b/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' - } - }) - .then(res => res.ok ? res.json() : Promise.reject(res.json())) - .then(() => { - this.setState({ showCustomIDError: true }) - }) - .catch(e => { - this.setState({ showCustomIDError: false }) - }) + util.lookupEntry(value, () => this.setState({ showCustomIDError: true }), () => 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({ - 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 - ]] - })) - .catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not fetch lookup: ${error.error}`)) : toastr.error(`Could not fetch create: ${e}`)) + util.createEntry({ + URL: this.url, + ID: this.customID, + 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 + }] + })) } } 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 (
@@ -99,12 +73,12 @@ export default class HomeComponent extends Component { - {setOptions.includes("custom") && + {usedSettings.includes("custom") && } - {setOptions.includes("expire") && + {usedSettings.includes("expire") && } minDate={moment()} /> } - {setOptions.includes("protected") && + {usedSettings.includes("protected") && } - {links.map((link, i) => )} + {links.map((link, i) => )}
) diff --git a/static/src/Lookup/Lookup.js b/static/src/Lookup/Lookup.js index ce3b7de..ab154fe 100644 --- a/static/src/Lookup/Lookup.js +++ b/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,28 +11,16 @@ 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({ - links: [...this.state.links, [ - res.URL, - this.url, - this.VisitCount, - res.CratedOn, - res.LastVisit, - res.Expiration - ]] - })) - .catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not fetch lookup: ${error.error}`)) : null) + util.lookupEntry(id, res => this.setState({ + links: [...this.state.links, [ + res.URL, + this.url, + this.VisitCount, + res.CratedOn, + res.LastVisit, + res.Expiration + ]] + })) } render() { const { links } = this.state @@ -42,7 +30,7 @@ export default class LookupComponent extends Component {
URL Lookup
- this.urlInput = input} action={{ icon: 'arrow right', labelPosition: 'right', content: 'Lookup' }} type='url' onChange={this.handleURLChange} name='url' placeholder={window.location.origin + "/..."} autoComplete="off"/> + this.urlInput = input} action={{ icon: 'arrow right', labelPosition: 'right', content: 'Lookup' }} type='url' onChange={this.handleURLChange} name='url' placeholder={window.location.origin + "/..."} autoComplete="off" />
diff --git a/static/src/Recent/Recent.js b/static/src/Recent/Recent.js index fca1ef1..8bc1bc7 100644 --- a/static/src/Recent/Recent.js +++ b/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 { - {recent && Object.keys(recent).map(key => + {Object.keys(recent).map(key => {recent[key].Public.URL} {recent[key].Public.CreatedOn} {`${window.location.origin}/${key}`} diff --git a/static/src/Visitors/Visitors.js b/static/src/Visitors/Visitors.js index 6a1d63b..9cb01c2 100644 --- a/static/src/Visitors/Visitors.js +++ b/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 ( - {info &&

- Entry with id '{id}' was created at {info.CreatedOn} and redirects to '{info.URL}'. Currently it has {visitors.length} visits. + {entry &&

+ Entry with id '{id}' was created at {entry.CreatedOn} and redirects to '{entry.URL}'. Currently it has {visitors.length} visits.

} diff --git a/static/src/index.js b/static/src/index.js index 9dd8806..d58a313 100644 --- a/static/src/index.js +++ b/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({ - userData: d, - authorized: true - }) - }) + .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 ( - + Authentication diff --git a/static/src/util/util.js b/static/src/util/util.js index ea1a7fb..296575d 100644 --- a/static/src/util/util.js +++ b/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")) } }; \ No newline at end of file