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 (
+
+
+ )
+ }
+}
+
+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
+
+
+
+ # |
+ URL |
+ Pageviews |
+ Unique |
+
+
+ {tableRows}
+
+
+ )
+ }
+}
+
+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 (
+
+ )
}
- 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 (
+
+
+ { 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"