diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..563ca8d --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,20 @@ +Ana Roadmap +=========== + +This is a general draft document for thoughts and todo's. + +### Key metrics + +- Unique visits per day (in period) +- Pageviews per day (in period) +- Demographic + - Country + - Browser + version + - Operating System +- Acquisition + + +### Admin themes + +- Pages, http://pages.revox.io/dashboard/latest/html/index.html#usa +- Metronic, http://keenthemes.com/preview/metronic/ diff --git a/api/api.go b/api/api.go index 59abda7..985da69 100644 --- a/api/api.go +++ b/api/api.go @@ -6,6 +6,7 @@ import ( ) func RegisterRoutes() { + http.HandleFunc("/api/visits/count/day", GetVisitsDayCountHandler) http.HandleFunc("/api/visits/count/realtime", GetVisitsRealtimeCount) http.HandleFunc("/api/visits", GetVisitsHandler) http.HandleFunc("/api/pageviews", GetPageviewsHandler) diff --git a/api/pageviews.go b/api/pageviews.go index 9326164..1687f95 100644 --- a/api/pageviews.go +++ b/api/pageviews.go @@ -11,7 +11,8 @@ import ( func GetPageviewsHandler(w http.ResponseWriter, r *http.Request) { stmt, err := core.DB.Prepare(`SELECT path, - COUNT(DISTINCT(ip_address)) AS pageviews + COUNT(ip_address) AS pageviews, + COUNT(DISTINCT(ip_address)) AS pageviews_unique FROM visits GROUP BY path`) checkError(err) @@ -24,7 +25,7 @@ func GetPageviewsHandler(w http.ResponseWriter, r *http.Request) { defer rows.Close() for rows.Next() { var p models.Pageview - err = rows.Scan(&p.Path, &p.Count); + err = rows.Scan(&p.Path, &p.Count, &p.CountUnique); checkError(err) results = append(results, p) } diff --git a/api/visits.go b/api/visits.go index e92a177..03a2b49 100644 --- a/api/visits.go +++ b/api/visits.go @@ -5,6 +5,7 @@ import ( "github.com/dannyvankooten/ana/models" "github.com/dannyvankooten/ana/core" "encoding/json" + "strconv" ) // URL: /api/visits @@ -18,12 +19,19 @@ func GetVisitsHandler(w http.ResponseWriter, r *http.Request) { path, COALESCE(screen_resolution, '') AS screen_resolution, timestamp - FROM visits`) + FROM visits + ORDER BY timestamp DESC + LIMIT ?`) checkError(err) defer stmt.Close() - rows, err := stmt.Query() + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + if limit == 0 { + limit = 10 + } + + rows, err := stmt.Query(&limit) checkError(err) results := make([]models.Visit, 0) @@ -44,10 +52,43 @@ func GetVisitsHandler(w http.ResponseWriter, r *http.Request) { // URL: /api/visits/count/realtime func GetVisitsRealtimeCount(w http.ResponseWriter, r *http.Request) { - row := core.DB.QueryRow(`SELECT COUNT(DISTINCT(id)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE)`) + row := core.DB.QueryRow(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE)`) var result int row.Scan(&result) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } + +// URL: /api/visits/count/day +func GetVisitsDayCountHandler(w http.ResponseWriter, r *http.Request) { + stmt, err := core.DB.Prepare(`SELECT + COUNT(*) AS count, DATE_FORMAT(timestamp, '%Y-%m-%d') AS date_group + FROM visits + GROUP BY date_group`) + checkError(err) + defer stmt.Close() + + rows, err := stmt.Query() + checkError(err) + + type Datapoint struct { + Count int + Label string + } + + results := make([]Datapoint, 0) + defer rows.Close() + for rows.Next() { + v := Datapoint{} + err = rows.Scan(&v.Count, &v.Label); + checkError(err) + results = append(results, v) + } + + err = rows.Err(); + checkError(err) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(results) +} diff --git a/assets/js/components/pageviews.js b/assets/js/components/pageviews.js index d15200b..a9910c5 100644 --- a/assets/js/components/pageviews.js +++ b/assets/js/components/pageviews.js @@ -19,20 +19,22 @@ class PageviewsList extends React.Component { const tableRows = this.state.records.map((p, i) => {i+1} - {p.Path} + {p.Path} {p.Count} + {p.CountUnique} ); return ( -
+

Pageviews

- - + + + {tableRows} diff --git a/assets/js/components/realtime-visits.js b/assets/js/components/realtime-visits.js index 9840a3f..c382c97 100644 --- a/assets/js/components/realtime-visits.js +++ b/assets/js/components/realtime-visits.js @@ -1,3 +1,5 @@ +'use strict'; + import React, { Component } from 'react' class RealtimeVisitsCount extends React.Component { diff --git a/assets/js/components/visits-graph.js b/assets/js/components/visits-graph.js new file mode 100644 index 0000000..56b8483 --- /dev/null +++ b/assets/js/components/visits-graph.js @@ -0,0 +1,75 @@ +'use strict'; + +import React, { Component } from 'react' +import Chart from 'chart.js' + +function rand(min, max, num) { + var rtn = []; + while (rtn.length < num) { + rtn.push((Math.random() * (max - min)) + min); + } + return rtn; + } + +class VisitsGraph extends React.Component { + constructor(props) { + super(props); + this.state = { data: [] } + this.refresh(); + } + + refresh() { + return fetch('/api/visits/count/day') + .then((r) => r.json()) + .then((data) => { + this.setState({ data: data }); + this.initChart(this.chartCtx); + }); + } + + initChart(ctx) { + if( ! this.state.data.length ) { + return; + } + + console.log(this.state.data.map((d) => d.Label)); + console.log(this.state.data.map((d) => d.Count )); + console.log("Init chart"); + console.log(ctx); + + var myChart = new Chart(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(162, 52, 235, 0.2)' + }] + }, + options: { + tooltips: { + xPadding: 12, + yPadding: 9, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero:true + } + }] + } + } + }); + } + + render() { + return ( +
+ { this.chartCtx = ctx; }} width="600" height="200"> +
+ ); + } +} + +export default VisitsGraph; diff --git a/assets/js/components/visits-list.js b/assets/js/components/visits-list.js index 5143e99..885462b 100644 --- a/assets/js/components/visits-list.js +++ b/assets/js/components/visits-list.js @@ -25,7 +25,7 @@ class VisitsList extends React.Component { ); return ( -
+

Visits

#PathCountURLPageviewsUnique
diff --git a/assets/js/script.js b/assets/js/script.js index 700dc12..2365871 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -5,20 +5,14 @@ import ReactDOM from 'react-dom'; import RealtimeVisitsCount from './components/realtime-visits.js'; import VisitsList from './components/visits-list.js'; import PageviewsList from './components/pageviews.js'; +import VisitsGraph from './components/visits-graph.js'; -function tick() { - const element = ( -
-

Ana

- - - -
- ); - - ReactDOM.render( - element, document.getElementById('root') - ); -} - -tick() && window.setInterval(tick, 1000); +ReactDOM.render( +
+

Ana

+ + + +
, + document.getElementById('root') +); diff --git a/assets/sass/_normalize.scss b/assets/sass/_normalize.scss new file mode 100644 index 0000000..9b77e0e --- /dev/null +++ b/assets/sass/_normalize.scss @@ -0,0 +1,461 @@ +/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Change the default font family in all browsers (opinionated). + * 2. Correct the line height in all browsers. + * 3. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +/* Document + ========================================================================== */ + +html { + font-family: sans-serif; /* 1 */ + line-height: 1.15; /* 2 */ + -ms-text-size-adjust: 100%; /* 3 */ + -webkit-text-size-adjust: 100%; /* 3 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * Remove the outline on focused links when they are also active or hovered + * in all browsers (opinionated). + */ + +a:active, +a:hover { + outline-width: 0; +} + +/** + * 1. Remove the bottom border in Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], /* 1 */ +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Change the border, margin, and padding in all browsers (opinionated). + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, /* 1 */ +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/assets/sass/styles.scss b/assets/sass/styles.scss index b534351..8cedb44 100644 --- a/assets/sass/styles.scss +++ b/assets/sass/styles.scss @@ -1,7 +1,10 @@ +@import "normalize"; + body { font-family: Arial, Verdana, sans-serif; font-size: 14px; color: #333; + background: #f5f5f5; } h1, h2, h3 { @@ -22,6 +25,16 @@ table { } } +.block { + width: auto; + background: white; + padding: 20px; + margin-bottom: 20px; + + h1, h2, h3 { + margin-top: 0; + } +} .count { font-size: 24px; diff --git a/models/pageview.go b/models/pageview.go index f5acfa5..871d7d5 100644 --- a/models/pageview.go +++ b/models/pageview.go @@ -3,4 +3,5 @@ package models type Pageview struct { Path string Count int + CountUnique int } diff --git a/package.json b/package.json index d570002..de6c93b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", "babelify": "^7.3.0", + "chart.js": "^2.4.0", "gulp-util": "^3.0.7", "react-dom": "^15.4.0" } diff --git a/yarn.lock b/yarn.lock index 3e778c8..2992094 100644 --- a/yarn.lock +++ b/yarn.lock @@ -842,6 +842,26 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" +chart.js: + version "2.4.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.4.0.tgz#44198073f0f43e5e16662e108420d92652a3c9a3" + dependencies: + chartjs-color "^2.0.0" + moment "^2.10.6" + +chartjs-color-string@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.4.0.tgz#57748d4530ae28d8db0a5492182ba06dfdf2f468" + dependencies: + color-name "^1.0.0" + +chartjs-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.0.0.tgz#7f60c7256589b24914814ece757659117381e35b" + dependencies: + chartjs-color-string "^0.4.0" + color-convert "^0.5.3" + cipher-base@^1.0.0, cipher-base@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" @@ -872,6 +892,14 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" +color-convert@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + +color-name@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" + combine-source-map@~0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e" @@ -2276,6 +2304,10 @@ module-deps@^4.0.8: through2 "^2.0.0" xtend "^4.0.0" +moment@^2.10.6: + version "2.17.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.0.tgz#a4c292e02aac5ddefb29a6eed24f51938dd3b74f" + ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"