From a7af934f23d211c5c44749fddabcdbfb0e5b9b80 Mon Sep 17 00:00:00 2001 From: Danny van Kooten Date: Wed, 23 Nov 2016 19:40:35 +0100 Subject: [PATCH] rewrite mithril to react --- ROADMAP.md | 6 ++ assets/js/components/Graph.js | 59 +++++++++++++++++++ assets/js/components/LoginForm.js | 59 +++++++++++++++++++ assets/js/components/LogoutButton.js | 33 +++++++++++ assets/js/components/Pageviews.js | 58 +++++++++++++++++++ assets/js/components/Realtime.js | 37 ++++++++++++ assets/js/components/login.js | 70 ---------------------- assets/js/components/logoutButton.js | 30 ---------- assets/js/components/pageviews.js | 49 ---------------- assets/js/components/realtime.js | 33 ----------- assets/js/components/visits-graph.js | 61 -------------------- assets/js/script.js | 86 +++++++++++++--------------- assets/sass/styles.scss | 6 ++ package.json | 4 +- yarn.lock | 31 +++++++++- 15 files changed, 329 insertions(+), 293 deletions(-) create mode 100644 assets/js/components/Graph.js create mode 100644 assets/js/components/LoginForm.js create mode 100644 assets/js/components/LogoutButton.js create mode 100644 assets/js/components/Pageviews.js create mode 100644 assets/js/components/Realtime.js delete mode 100644 assets/js/components/login.js delete mode 100644 assets/js/components/logoutButton.js delete mode 100644 assets/js/components/pageviews.js delete mode 100644 assets/js/components/realtime.js delete mode 100644 assets/js/components/visits-graph.js diff --git a/ROADMAP.md b/ROADMAP.md index e6fc60b..e967b0b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,6 +15,12 @@ This is a general draft document for thoughts and todo's. - Referral's - Search keywords +## Notes + +- Every API response has to be JSON +- Envelope every response with `data` and `message` props. +- JS client for consuming API. + ### Admin themes diff --git a/assets/js/components/Graph.js b/assets/js/components/Graph.js new file mode 100644 index 0000000..48dbfd7 --- /dev/null +++ b/assets/js/components/Graph.js @@ -0,0 +1,59 @@ +'use strict'; + +import { h, render, Component } from 'preact'; +import Chart from 'chart.js' + +class Graph extends Component { + + constructor(props) { + super(props) + + this.state = { + data: [] + } + this.fetchData = this.fetchData.bind(this); + this.fetchData(); + } + + initChart() { + new Chart(this.ctx, { + type: 'line', + data: { + labels: this.state.data.map((d) => d.Label), + datasets: [{ + label: '# of Visitors', + data: this.state.data.map((d) => d.Count), + backgroundColor: 'rgba(0, 155, 255, .2)' + }] + }, + options: { + scale: { + ticks: { + beginAtZero: true + } + } + } + }); + } + + fetchData() { + return fetch('/api/visits/count/day', { + credentials: 'include' + }) + .then((r) => r.json()) + .then((data) => { + this.setState({ data: data}) + this.initChart() + }); + } + + render() { + return ( +
+ { this.ctx = el; }} /> +
+ ) + } +} + +export default Graph diff --git a/assets/js/components/LoginForm.js b/assets/js/components/LoginForm.js new file mode 100644 index 0000000..fd93b09 --- /dev/null +++ b/assets/js/components/LoginForm.js @@ -0,0 +1,59 @@ +'use strict'; + +import { h, render, Component } from 'preact'; + +class LoginForm extends Component { + + constructor(props) { + super(props) + this.handleSubmit = this.handleSubmit.bind(this); + this.setState({ + email: '', + password: '', + }) + } + + handleSubmit(e) { + e.preventDefault(); + + fetch('/api/session', { + method: "POST", + data: { + email: this.state.email, + password: this.state.password, + }, + credentials: 'include' + }).then((r) => { + if( r.status == 200 ) { + this.props.onAuth(); + console.log("Authenticated!"); + } + + // TODO: Handle errors + }); + } + + render() { + return ( +
+

Login

+

Please enter your credentials to access your Ana dashboard.

+
+
+ + +
+
+ + +
+
+ +
+
+
+ ) + } +} + +export default LoginForm diff --git a/assets/js/components/LogoutButton.js b/assets/js/components/LogoutButton.js new file mode 100644 index 0000000..dd6cbc4 --- /dev/null +++ b/assets/js/components/LogoutButton.js @@ -0,0 +1,33 @@ +'use strict'; + +import { h, render, Component } from 'preact'; + +class LogoutButton extends Component { + + constructor(props) { + super(props) + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleSubmit(e) { + e.preventDefault(); + + fetch('/api/session', { + method: "DELETE", + credentials: 'include', + }).then((r) => { + if( r.status == 200 ) { + this.props.onSuccess(); + console.log("No longer authenticated!"); + } + }); + } + + render() { + return ( + Sign out + ) + } +} + +export default LogoutButton diff --git a/assets/js/components/Pageviews.js b/assets/js/components/Pageviews.js new file mode 100644 index 0000000..c19cf0f --- /dev/null +++ b/assets/js/components/Pageviews.js @@ -0,0 +1,58 @@ +'use strict'; + +import { h, render, Component } from 'preact'; + +class Pageviews extends Component { + + constructor(props) { + super(props) + + this.state = { + records: [] + } + this.fetchRecords = this.fetchRecords.bind(this); + this.fetchRecords(); + } + + fetchRecords() { + return fetch('/api/pageviews', { + credentials: 'include' + }).then((r) => { + if( r.ok ) { + return r.json(); + } + }).then((data) => { + this.setState({ records: data }) + }); + } + + render() { + const tableRows = this.state.records.map( (p, i) => ( + + {i+1} + {p.Path} + {p.Count} + {p.CountUnique} + + )); + + return ( +
+

Pageviews

+ + + + + + + + + + {tableRows} +
#URLPageviewsUnique
+
+ ) + } +} + +export default Pageviews diff --git a/assets/js/components/Realtime.js b/assets/js/components/Realtime.js new file mode 100644 index 0000000..1d2ed15 --- /dev/null +++ b/assets/js/components/Realtime.js @@ -0,0 +1,37 @@ +'use strict'; + +import { h, render, Component } from 'preact'; + +class Realtime extends Component { + + constructor(props) { + super(props) + + this.state = { + count: 0 + } + this.fetchData = this.fetchData.bind(this); + this.fetchData(); + } + + fetchData() { + return fetch('/api/visits/count/realtime', { + credentials: 'include' + }) + .then((r) => r.json()) + .then((data) => { + this.setState({ count: data }) + }); + } + + render() { + let visitors = this.state.count == 1 ? 'visitor' : 'visitors'; + return ( +
+ {this.state.count} {visitors} on the site right now +
+ ) + } +} + +export default Realtime diff --git a/assets/js/components/login.js b/assets/js/components/login.js deleted file mode 100644 index 0d37e9d..0000000 --- a/assets/js/components/login.js +++ /dev/null @@ -1,70 +0,0 @@ -import m from 'mithril'; - -function handleSubmit(e) { - e.preventDefault(); - - fetch('/api/session', { - method: "POST", - data: { - email: this.data.email(), - password: this.data.password() - }, - credentials: 'include' - }).then((r) => { - if( r.status == 200 ) { - this.onAuth(); - console.log("Authenticated!"); - } - - // TODO: Handle errors - }); -} - -const Login = { - controller(args) { - this.onAuth = args.onAuth; - this.data = { - email: m.prop(''), - password: m.prop(''), - } - this.onSubmit = handleSubmit.bind(this); - }, - - view(c) { - return m('div.block', [ - m('h2', 'Login'), - m('p', 'Please enter your credentials to login to your Ana dashboard.'), - m('form', { - method: "POST", - onsubmit: c.onSubmit - }, [ - m('div.form-group', [ - m('label', 'Email address'), - m('input', { - type: "email", - name: "email", - required: true, - onchange: m.withAttr("value", c.data.email ) - }), - ]), - m('div.form-group', [ - m('label', 'Password'), - m('input', { - type: "password", - name: "password", - required: true, - onchange: m.withAttr("value", c.data.password ) - }), - ]), - m('div.form-group', [ - m('input', { - type: "submit", - value: "Sign in" - }), - ]), - ]) - ]) - } -} - -export default Login diff --git a/assets/js/components/logoutButton.js b/assets/js/components/logoutButton.js deleted file mode 100644 index 67a9ddf..0000000 --- a/assets/js/components/logoutButton.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -import m from 'mithril'; - -function handleSubmit(e) { - e.preventDefault(); - - fetch('/api/session', { - method: "DELETE", - credentials: 'include', - }).then((r) => { - if( r.status == 200 ) { - this.cb(); - console.log("No longer authenticated!"); - } - }); -} - -const LogoutButton = { - controller(args) { - this.cb = args.cb; - this.onSubmit = handleSubmit.bind(this); - }, - - view(c) { - return m('a', { href: "#", onclick: c.onSubmit }, 'Sign out'); - } -} - -export default LogoutButton diff --git a/assets/js/components/pageviews.js b/assets/js/components/pageviews.js deleted file mode 100644 index 1958e85..0000000 --- a/assets/js/components/pageviews.js +++ /dev/null @@ -1,49 +0,0 @@ -import m from 'mithril'; - -function fetchRecords() { - return fetch('/api/pageviews', { - credentials: 'include' - }).then((r) => { - if( r.ok ) { - return r.json(); - } - }).then((data) => { - m.startComputation(); - this.records(data); - m.endComputation(); - }); -} - -const Pageviews = { - controller() { - this.records = m.prop([]); - fetchRecords.call(this) && window.setInterval(fetchRecords.bind(this), 60000); - }, - view(c) { - const tableRows = c.records().map((p, i) => m('tr', [ - m('td', i+1), - m('td', [ - m('a', { href: p.Path }, p.Path) - ]), - m('td', p.Count), - m('td', p.CountUnique) - ])); - - return m('div.block', [ - m('h2', 'Pageviews'), - m('table.table.pageviews', [ - m('thead', [ - m('tr', [ - m('th', '#'), - m('th', 'URL'), - m('th', 'Pageviews'), - m('th', 'Unique'), - ]) // tr - ]), // thead - m('tbody', tableRows ) - ]) // table - ]) - } -} - -export default Pageviews diff --git a/assets/js/components/realtime.js b/assets/js/components/realtime.js deleted file mode 100644 index 288b7f2..0000000 --- a/assets/js/components/realtime.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import m from 'mithril'; - -function fetchData() { - return fetch('/api/visits/count/realtime', { - credentials: 'include' - }) - .then((r) => r.json()) - .then((data) => { - this.count = data; - m.redraw(); - }); -} - -const RealtimeVisits = { - controller(args) { - this.count = 0; - fetchData.bind(this) && window.setInterval(fetchData.bind(this), 6000); - }, - - view(c) { - let visitors = c.count > 1 ? 'visitors' : 'visitor'; - - return m('div.block', [ - m('span.count', c.count), - ' ', - m('span', visitors + " on the site right now.") - ]) - } -} - -export default RealtimeVisits diff --git a/assets/js/components/visits-graph.js b/assets/js/components/visits-graph.js deleted file mode 100644 index b13e883..0000000 --- a/assets/js/components/visits-graph.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict' - -import m from 'mithril'; -import Chart from 'chart.js' - -Chart.defaults.global.tooltips.xPadding = 12; -Chart.defaults.global.tooltips.yPadding = 12; - -function fetchData() { - return fetch('/api/visits/count/day', { - credentials: 'include' - }) - .then((r) => r.json()) - .then((data) => { - this.data = data; - initChart.call(this); - }); -} - -function initChart() { - let ctx = this.chartCtx; - - new Chart(ctx, { - type: 'line', - data: { - labels: this.data.map((d) => d.Label), - datasets: [{ - label: '# of Visitors', - data: this.data.map((d) => d.Count), - backgroundColor: 'rgba(0, 155, 255, .2)' - }] - }, - options: { - scale: { - ticks: { - beginAtZero: true - } - } - } - }); -} - -const VisitsGraph = { - controller(args) { - this.data = []; - fetchData.call(this); - }, - - view(c) { - return m('div.block', [ - m('canvas', { - width: 600, - height: 200, - config: (el) => { c.chartCtx = el; } - }) - ]) - }, - -} - -export default VisitsGraph; diff --git a/assets/js/script.js b/assets/js/script.js index 78c1c5a..59f1a97 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -1,56 +1,50 @@ 'use strict'; -const m = require('mithril'); -import Login from './components/login.js'; -import Pageviews from './components/pageviews.js'; -import RealtimeVisits from './components/realtime.js'; -import VisitsGraph from './components/visits-graph.js'; -import LogoutButton from './components/logoutButton.js'; +import { h, render, Component } from 'preact'; +import LoginForm from './components/LoginForm.js'; +import LogoutButton from './components/LogoutButton.js'; +import Pageviews from './components/Pageviews.js'; +import Realtime from './components/Realtime.js'; +import Graph from './components/Graph.js'; + +class App extends Component { + constructor(props) { + super(props) -const App = { - controller(args) { this.state = { - authenticated: document.cookie.indexOf('auth') > -1 - }; - - this.setState = function(nextState) { - m.startComputation(); - for(var k in nextState) { - this.state[k] = nextState[k]; - } - m.endComputation(); + authenticated: document.cookie.indexOf('auth') > -1, } - }, - view(c) { - if( ! c.state.authenticated ) { - return m('div.container', [ - m.component(Login, { - onAuth: () => { - c.setState({ authenticated: true }) - } - }) - ]); + } + + render() { + + // logged-in + if( this.state.authenticated ) { + return ( +
+
+

Ana open web analytics

+
+ { this.setState({ authenticated: false })}} /> +
+
+ + + +
+ ) } - return [ - m('div.container', [ - m('div.header.cf', [ - m('h1.pull-left', 'Ana'), - m('div.pull-right', [ - m.component(LogoutButton, { - cb: () => { - c.setState({ authenticated: false }) - } - }) - ]), - ]), - m.component(RealtimeVisits), - m.component(VisitsGraph), - m.component(Pageviews), - ]) - ] + // logged-out + return ( +
+
+

Ana open web analytics

+
+ { this.setState({ authenticated: true })}} /> +
+ ) } } - -m.mount(document.getElementById('root'), App) +render(, document.getElementById('root')); diff --git a/assets/sass/styles.scss b/assets/sass/styles.scss index ce82aff..109fefb 100644 --- a/assets/sass/styles.scss +++ b/assets/sass/styles.scss @@ -9,6 +9,12 @@ body { h1, h2, h3 { color: #111; + + small { + font-weight: normal; + font-style: italic; + color: #666; + } } table { diff --git a/package.json b/package.json index f88b7f2..42e68f9 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,11 @@ "vinyl-source-stream": "^1.1.0" }, "dependencies": { + "babel-plugin-transform-react-jsx": "^6.8.0", "babel-preset-es2015": "^6.18.0", "babelify": "^7.3.0", "chart.js": "^2.4.0", - "mithril": "^0.2.5" + "mithril": "^0.2.5", + "preact": "^6.4.0" } } diff --git a/yarn.lock b/yarn.lock index 63a9ee8..a256fc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -176,6 +176,15 @@ babel-generator@^6.18.0: lodash "^4.2.0" source-map "^0.5.0" +babel-helper-builder-react-jsx@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.18.0.tgz#ab02f19a2eb7ace936dd87fa55896d02be59bf71" + dependencies: + babel-runtime "^6.9.0" + babel-types "^6.18.0" + esutils "^2.0.0" + lodash "^4.2.0" + babel-helper-call-delegate@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.18.0.tgz#05b14aafa430884b034097ef29e9f067ea4133bd" @@ -263,6 +272,10 @@ babel-plugin-check-es2015-constants@^6.3.13: dependencies: babel-runtime "^6.0.0" +babel-plugin-syntax-jsx@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + babel-plugin-transform-es2015-arrow-functions@^6.3.13: version "6.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.8.0.tgz#5b63afc3181bdc9a8c4d481b5a4f3f7d7fef3d9d" @@ -432,6 +445,14 @@ babel-plugin-transform-es2015-unicode-regex@^6.3.13: babel-runtime "^6.0.0" regexpu-core "^2.0.0" +babel-plugin-transform-react-jsx: + version "6.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.8.0.tgz#94759942f70af18c617189aa7f3593f1644a71ab" + dependencies: + babel-helper-builder-react-jsx "^6.8.0" + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.0.0" + babel-plugin-transform-regenerator@^6.16.0: version "6.16.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.16.1.tgz#a75de6b048a14154aae14b0122756c5bed392f59" @@ -528,7 +549,7 @@ babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.8. lodash "^4.2.0" to-fast-properties "^1.0.1" -babelify: +babelify@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/babelify/-/babelify-7.3.0.tgz#aa56aede7067fd7bd549666ee16dc285087e88e5" dependencies: @@ -1119,7 +1140,7 @@ escape-string-regexp@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -esutils@^2.0.2: +esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -2174,7 +2195,7 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -mithril: +mithril@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/mithril/-/mithril-0.2.5.tgz#c1a50438a93ac23f11ada91188bb784c755404c2" @@ -2480,6 +2501,10 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" +preact: + version "6.4.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-6.4.0.tgz#1b8c99754b002639a1c33e68175eefa98a01cc14" + preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"