first stab at new design, needs work

This commit is contained in:
Danny 2018-05-01 16:06:17 +02:00
parent c238a2abc3
commit 8137f10fbc
19 changed files with 245 additions and 1399 deletions

View File

@ -4,7 +4,7 @@
<title>Fathom - simple website analytics</title> <title>Fathom - simple website analytics</title>
<link href="/css/styles.css" rel="stylesheet"> <link href="/css/styles.css" rel="stylesheet">
</head> </head>
<body> <body class="fathom">
<div id="root"></div> <div id="root"></div>
<script> <script>
document.documentElement.className = document.documentElement.className.replace('no-js', ''); document.documentElement.className = document.documentElement.className.replace('no-js', '');

View File

@ -3,6 +3,14 @@
import { h, render, Component } from 'preact'; import { h, render, Component } from 'preact';
import * as numbers from '../lib/numbers.js'; import * as numbers from '../lib/numbers.js';
import Client from '../lib/client.js'; import Client from '../lib/client.js';
import { bind } from 'decko';
function getSundayOfCurrentWeek(d){
var day = d.getDay();
return new Date(d.getFullYear(), d.getMonth(), d.getDate() + (day == 0?0:7)-day );
}
const dayInSeconds = 60 * 60 * 24; const dayInSeconds = 60 * 60 * 24;
class CountWidget extends Component { class CountWidget extends Component {
@ -10,55 +18,57 @@ class CountWidget extends Component {
super(props) super(props)
this.state = { this.state = {
count: 0, value: '-',
previousCount: 0,
loading: false loading: false
} }
this.fetchData = this.fetchData.bind(this);
this.fetchData(props.period); this.fetchData(props.period);
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
console.log(newProps);
if(this.props.period != newProps.period) { if(this.props.period != newProps.period) {
this.fetchData(newProps.period) this.fetchData(newProps.period)
} }
} }
@bind
fetchData(period) { fetchData(period) {
const before = Math.round((+new Date() ) / 1000); let before, after;
const after = before - ( period * dayInSeconds ); let afterDate = new Date();
afterDate.setHours(0, 0, 0, 0);
switch(period) {
case "week":
afterDate.setDate(afterDate.getDate() - (afterDate.getDay() + 6) % 7);
break;
case "month":
afterDate.setDate(1);
break;
case "year":
afterDate.setDate(1);
afterDate.setMonth(0);
break;
}
before = Math.round((+new Date() ) / 1000);
after = Math.round((+afterDate) / 1000);
this.setState({ loading: true }) this.setState({ loading: true })
Client.request(`${this.props.endpoint}/count?before=${before}&after=${after}`) Client.request(`${this.props.endpoint}/count?before=${before}&after=${after}`)
.then((d) => { this.setState({ loading: false, count: d })}) .then((d) => {
this.setState({
// query previous period loading: false,
const previousBefore = after; value: numbers.formatWithComma(d),
const previousAfter = previousBefore - ( period * dayInSeconds ); })
Client.request(`${this.props.endpoint}/count?before=${previousBefore}&after=${previousAfter}`) })
.then((d) => { this.setState({ previousCount: d })})
} }
renderPercentage() { render(props, state) {
// wait for request to finish const loadingOverlay = state.loading ? <div class="loading-overlay"><div></div></div> : '';
if( ! this.state.previousCount ) {
return '';
}
const percentage = Math.round(( this.state.count / this.state.previousCount * 100 - 100))
return ( return (
<small class={percentage > 0 ? 'positive' : 'negative'}>{percentage}%</small> <div class="totals-detail">
)
}
render() {
const loadingOverlay = this.state.loading ? <div class="loading-overlay"><div></div></div> : '';
return (
<div class="block center-text">
{loadingOverlay} {loadingOverlay}
<h4 class="">{this.props.title}</h4> <div class="total-heading">{props.title}</div>
<div class="big tiny-margin">{numbers.formatWithComma(this.state.count)} {this.renderPercentage()}</div> <div class="total-numbers">{state.value}</div>
</div> </div>
) )
} }

View File

@ -1,23 +1,24 @@
'use strict'; 'use strict';
import { h, render, Component } from 'preact'; import { h, render, Component } from 'preact';
import { bind } from 'decko';
const availablePeriods = [ const availablePeriods = [
{ {
id: 7, id: 'day',
label: 'Last 7 days' label: 'Today'
}, },
{ {
id: 30, id: 'week',
label: 'Last 30 days' label: 'This week'
}, },
{ {
id: 90, id: 'month',
label: 'Last quarter' label: 'This month'
}, },
{ {
id: 360, id: 'year',
label: 'Last year' label: 'This year'
} }
] ]
@ -26,31 +27,35 @@ class DatePicker extends Component {
super(props) super(props)
this.state = { this.state = {
period: this.props.period period: this.props.value
} }
this.setPeriod = this.setPeriod.bind(this)
} }
@bind
setPeriod(e) { setPeriod(e) {
var nextState = { period: parseInt(e.target.value) } e.preventDefault();
var nextState = {
period: e.target.dataset.value
}
if(this.state.period != nextState.period) { if(this.state.period != nextState.period) {
this.setState(nextState) this.setState(nextState)
this.props.onChoose(this.state.period); this.props.onChange(nextState.period);
} }
} }
render() { render(props, state) {
const buttons = availablePeriods.map((p) => { const links = availablePeriods.map((p) => {
let className = ( p.id == this.state.period ) ? 'active' : ''; let className = ( p.id == state.period ) ? 'active' : '';
return <button value={p.id} class={className} onClick={this.setPeriod}>{p.label}</button> return <li class={className} ><a href="#" data-value={p.id} onClick={this.setPeriod}>{p.label}</a></li>
}); });
return ( return (
<div class="small-margin"> <ul>
{buttons} {links}
</div> </ul>
) )
} }
} }

View File

@ -1,190 +0,0 @@
'use strict';
import { h, render, Component } from 'preact';
import Client from '../lib/client.js';
import * as numbers from '../lib/numbers.js';
import * as d3 from 'd3';
d3.tip = require('d3-tip');
const dayInSeconds = 60 * 60 * 24;
function Chart(element, showPrimary, showSecondary) {
var padt = 10, padb = 20, padr = 40, padl = 40,
h = 300,
w = element.parentNode.clientWidth - padl - padr,
x = d3.scaleBand().range([0, w]).padding(0.2).round(true),
y = d3.scaleLinear().range([h, 0]),
yAxis = d3.axisLeft().scale(y).tickSize(-w + padl + padr),
xAxis = d3.axisBottom().scale(x),
primaryData = [],
secondaryData = [];
var pageviewTip = d3.tip()
.attr('class', 'd3-tip')
.html((d) => '<h5>'+ d.Label +'</h5><span>' + numbers.formatWithComma(d.Value) + '</span>' + ' pageviews')
.offset([-12, 0]);
var visitorTip = d3.tip()
.attr('class', 'd3-tip')
.html((d) => '<h5>'+ d.Label +'</h5><span>' + numbers.formatWithComma(d.Value) + '</span>' + ' visitors' )
.offset([-12, 0]);
var graph = d3.select('#graph');
var vis = graph
.append('svg')
.attr('width', w + padl + padr)
.attr('height', h + padt + padb)
.append('g')
.attr('transform', 'translate(' + padl + ',' + padt + ')');
vis.call(pageviewTip);
vis.call(visitorTip);
function setData(one, two) {
primaryData = one;
secondaryData = two;
}
function toggleBars(one, two) {
showPrimary = one;
showSecondary = two;
}
function draw() {
var max = d3.max(showPrimary ? primaryData : secondaryData, (d) => d.Value);
var ticks = primaryData.length;
var xTick = Math.round(ticks / 7);
x.domain(primaryData.map((d) => d.Label))
y.domain([0, (max * 1.1)])
// clear all previous data
vis.selectAll('*').remove();
// axes
vis.append("g")
.attr("class", "y axis")
.call(yAxis);
vis.append("g")
.attr("class", "x axis")
.attr('transform', 'translate(0,' + h + ')')
.call(xAxis)
.selectAll('g')
.style('display', (d, i) => i % xTick != 0 ? 'none' : 'block')
// bars
if( showPrimary ) {
var bars = vis.selectAll('g.primary-bar')
.data(primaryData)
.enter()
.append('g')
.attr('class', 'primary-bar')
.attr('transform', function (d, i) { return "translate(" + x(d.Label) + ", 0)" });
bars.append('rect')
.attr('width', x.bandwidth())
.attr('height', (d) => (h - y(d.Value)) )
.attr('y', (d) => y(d.Value))
.on('mouseover', pageviewTip.show)
.on('mouseout', pageviewTip.hide);
}
if(showSecondary) {
var visitorBars = vis.selectAll('g.sub-bar')
.data(secondaryData)
.enter()
.append('g')
.attr('class', 'sub-bar')
.attr('transform', (d, i) => "translate(" + ( x(d.Label) + ( x.bandwidth() * 0.16667 ) ) + ", 0)");
visitorBars.append('rect')
.attr('width', x.bandwidth() * 0.66 )
.attr('height', (d) => (h - y(d.Value)) )
.attr('y', (d) => y(d.Value))
.on('mouseover', visitorTip.show)
.on('mouseout', visitorTip.hide);
}
}
return {
'draw': draw,
'setData': setData,
'toggleBars': toggleBars,
}
}
class Graph extends Component {
constructor(props) {
super(props)
this.fetchData = this.fetchData.bind(this);
this.refreshChart = this.refreshChart.bind(this);
this.data = {
visitors: null,
pageviews: null,
}
}
componentDidMount() {
this.chart = new Chart(document.getElementById('graph'), this.props.showPageviews, this.props.showVisitors)
this.fetchData(this.props.period)
}
componentWillReceiveProps(newProps) {
if(this.props.period != newProps.period) {
this.fetchData(newProps.period)
}
this.chart.toggleBars(newProps.showPageviews, newProps.showVisitors)
this.chart.draw()
}
shouldComponentUpdate() {
return false
}
refreshChart() {
if(this.data.visitors && this.data.pageviews) {
this.loadingIndicator.style.display = 'none';
this.chart.setData(this.data.pageviews, this.data.visitors)
this.chart.draw()
}
}
fetchData(period) {
const before = Math.round((+new Date() ) / 1000);
const after = before - ( period * dayInSeconds );
const group = period > 90 ? 'month' : 'day';
this.loadingIndicator.style.display = '';
Client
.request(`pageviews/count/group/${group}?before=${before}&after=${after}`)
.then((d) => {
this.data.pageviews = d;
window.requestAnimationFrame(this.refreshChart);
})
Client
.request(`visitors/count/group/${group}?before=${before}&after=${after}`)
.then((d) => {
this.data.visitors = d;
window.requestAnimationFrame(this.refreshChart);
})
}
render() {
return (
<div>
<div class="loading-overlay" ref={(el) => { this.loadingIndicator = el }}><div></div></div>
<div id="graph"></div>
</div>
)
}
}
export default Graph

View File

@ -1,29 +0,0 @@
'use strict';
import { h, render, Component } from 'preact';
import Graph from './Graph.js';
class GraphWidget extends Component {
constructor(props) {
super(props)
this.state = {
showPageviews: true,
showVisitors: true,
}
}
render() {
return (
<div class="block">
<div class="pull-right">
<label class="inline small-margin-right"><input type="checkbox" checked={this.state.showPageviews} onchange={(e) => this.setState({ showPageviews: e.target.checked })} /> Pageviews</label>
<label class="inline"><input type="checkbox" checked={this.state.showVisitors} onchange={(e) => this.setState({ showVisitors: e.target.checked })} /> Visitors</label>
</div>
<Graph period={this.props.period} showPageviews={this.state.showPageviews} showVisitors={this.state.showVisitors} />
</div>
)
}
}
export default GraphWidget

View File

@ -1,22 +0,0 @@
'use strict'
import { h, render, Component } from 'preact';
import LogoutButton from '../components/LogoutButton.js';
class HeaderBar extends Component {
render() {
const rightContent = this.props.showLogout ? <LogoutButton onSuccess={this.props.onLogout} /> : '';
return (
<header class="header-bar cf">
<div class="container">
<h1 class="pull-left title">Fathom <small class="subtitle">simple website analytics</small></h1>
<div class="pull-right">
{rightContent}
</div>
</div>
</header>
)}
}
export default HeaderBar

View File

@ -3,7 +3,7 @@
import { h, render, Component } from 'preact'; import { h, render, Component } from 'preact';
import Client from '../lib/client.js'; import Client from '../lib/client.js';
import Notification from '../components/Notification.js'; import Notification from '../components/Notification.js';
import { bind, memoize, debounce } from 'decko'; import { bind } from 'decko';
class LoginForm extends Component { class LoginForm extends Component {

View File

@ -1,72 +0,0 @@
'use strict';
import { h, render, Component } from 'preact';
import * as numbers from '../lib/numbers.js';
import Client from '../lib/client.js';
const dayInSeconds = 60 * 60 * 24;
class Pageviews extends Component {
constructor(props) {
super(props)
this.state = {
records: [],
loading: false
}
this.fetchRecords = this.fetchRecords.bind(this);
}
componentDidMount() {
this.fetchRecords(this.props.period)
}
componentWillReceiveProps(newProps) {
if(this.props.period != newProps.period) {
this.fetchRecords(newProps.period)
}
}
fetchRecords(period) {
const before = Math.round((+new Date() ) / 1000);
const after = before - ( period * dayInSeconds );
this.setState({ loading: true })
Client.request(`pageviews?before=${before}&after=${after}`)
.then((d) => { this.setState({ loading: false, records: d })})
.catch((e) => { console.log(e) })
}
render() {
const loadingOverlay = this.state.loading ? <div class="loading-overlay"><div></div></div> : '';
const tableRows = this.state.records !== null ? this.state.records.map( (p, i) => (
<tr>
<td class="muted">{i+1}</td>
<td><a href={"//" + p.Hostname + p.Path}>{p.Path.substring(0, 50)}{p.Path.length > 50 ? '..' : ''}</a></td>
<td>{numbers.formatWithComma(p.Count)}</td>
<td>{numbers.formatWithComma(p.CountUnique)}</td>
</tr>
)) : <tr><td colspan="4" class="italic">Nothing here, yet..</td></tr>;;
return (
<div class="block">
{loadingOverlay}
<h3>Pageviews</h3>
<table class="table pageviews">
<thead>
<tr>
<th>#</th>
<th>URL</th>
<th>Pageviews</th>
<th>Unique</th>
</tr>
</thead>
<tbody>{tableRows}</tbody>
</table>
</div>
)
}
}
export default Pageviews

View File

@ -2,6 +2,7 @@
import { h, render, Component } from 'preact'; import { h, render, Component } from 'preact';
import Client from '../lib/client.js'; import Client from '../lib/client.js';
import { bind } from 'decko';
class Realtime extends Component { class Realtime extends Component {
@ -11,11 +12,11 @@ class Realtime extends Component {
this.state = { this.state = {
count: 0 count: 0
} }
this.fetchData = this.fetchData.bind(this);
this.fetchData(); this.fetchData();
window.setInterval(this.fetchData, 15000); window.setInterval(this.fetchData, 15000);
} }
@bind
fetchData() { fetchData() {
Client.request(`visitors/count/realtime`) Client.request(`visitors/count/realtime`)
.then((d) => { this.setState({ count: d })}) .then((d) => { this.setState({ count: d })})
@ -24,9 +25,7 @@ class Realtime extends Component {
render() { render() {
let visitors = this.state.count == 1 ? 'visitor' : 'visitors'; let visitors = this.state.count == 1 ? 'visitor' : 'visitors';
return ( return (
<div class="block block-float"> <span><span class="count">{this.state.count}</span> <span>current {visitors}</span></span>
<span class="count">{this.state.count}</span> <span>{visitors} on the site right now.</span>
</div>
) )
} }
} }

View File

@ -3,6 +3,8 @@
import { h, render, Component } from 'preact'; import { h, render, Component } from 'preact';
import * as numbers from '../lib/numbers.js'; import * as numbers from '../lib/numbers.js';
import Client from '../lib/client.js'; import Client from '../lib/client.js';
import { bind } from 'decko';
const dayInSeconds = 60 * 60 * 24; const dayInSeconds = 60 * 60 * 24;
class Table extends Component { class Table extends Component {
@ -12,40 +14,22 @@ class Table extends Component {
this.state = { this.state = {
records: [], records: [],
limit: 5, limit: 100,
loading: true loading: true
} }
this.tableHeaders = props.headers.map(heading => <th>{heading}</th>)
this.fetchRecords = this.fetchRecords.bind(this)
this.handleLimitChoice = this.handleLimitChoice.bind(this)
} }
componentDidMount() { componentDidMount() {
this.fetchRecords(this.props.period, this.state.limit) this.fetchRecords(this.props.period, this.state.limit)
} }
labelCell(p) {
if( this.props.labelCell ) {
return this.props.labelCell(p)
}
return (
<td>{p.Label.substring(0, 15)}</td>
)
}
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if(this.props.period != newProps.period) { if(this.props.period != newProps.period) {
this.fetchRecords(newProps.period, this.state.limit) this.fetchRecords(newProps.period, this.state.limit)
} }
} }
handleLimitChoice(e) { @bind
this.setState({ limit: parseInt(e.target.value) })
this.fetchRecords(this.props.period, this.state.limit)
}
fetchRecords(period, limit) { fetchRecords(period, limit) {
this.setState({ loading: true }); this.setState({ loading: true });
const before = Math.round((+new Date() ) / 1000); const before = Math.round((+new Date() ) / 1000);
@ -54,42 +38,31 @@ class Table extends Component {
Client.request(`${this.props.endpoint}?before=${before}&after=${after}&limit=${limit}`) Client.request(`${this.props.endpoint}?before=${before}&after=${after}&limit=${limit}`)
.then((d) => { .then((d) => {
this.setState({ loading: false, records: d } this.setState({ loading: false, records: d }
)}).catch((e) => { console.log(e) }) )})
} }
render() { render(props, state) {
const tableRows = this.state.records !== null ? this.state.records.map((p, i) => ( const tableRows = state.records !== null ? state.records.map((p, i) => (
<tr> <div class="table-row">
<td class="muted">{i+1}</td> <div class="cell main-col"><a href="#">/about-us/</a></div>
{this.labelCell(p)} <div class="cell">445.2k</div>
<td>{numbers.formatWithComma(p.Value)}</td> <div class="cell">5,456</div>
<td>{Math.round(p.PercentageValue)}%</td> </div>
</tr> )) : <div class="table-row">Nothing here, yet.</div>;
)) :<tr><td colspan="4" class="italic">Nothing here..</td></tr>;
const loadingOverlay = this.state.loading ? <div class="loading-overlay"><div></div></div> : ''; const loadingOverlay = state.loading ? <div class="loading-overlay"><div></div></div> : '';
return ( return (
<div class="block"> <div class="box box-pages animated fadeInUp delayed_04s">
{loadingOverlay}
<div class="clearfix"> <div class="table-row header">
<h3 class="pull-left">{this.props.title}</h3> {props.headers.map((header, i) => {
<div class="pull-right"> let classes = i === 0 ? 'main-col cell' : 'cell';
<select onchange={this.handleLimitChoice}> return (<div class={classes}>{header}</div>)
<option>5</option> })}
<option>20</option> </div>
<option>100</option>
</select> {tableRows}
</div>
</div>
<table>
<thead>
<tr>{this.tableHeaders}</tr>
</thead>
<tbody>
{tableRows}
</tbody>
</table>
</div> </div>
) )
} }

View File

@ -1,84 +1,65 @@
'use strict' 'use strict'
import { h, render, Component } from 'preact'; import { h, render, Component } from 'preact';
import Pageviews from '../components/Pageviews.js'; import LogoutButton from '../components/LogoutButton.js';
import Realtime from '../components/Realtime.js'; import Realtime from '../components/Realtime.js';
import GraphWidget from '../components/GraphWidget.js';
import DatePicker from '../components/DatePicker.js'; import DatePicker from '../components/DatePicker.js';
import Table from '../components/Table.js';
import HeaderBar from '../components/HeaderBar.js';
import CountWidget from '../components/CountWidget.js'; import CountWidget from '../components/CountWidget.js';
import Table from '../components/Table.js';
function removeImgElement(e) { import { bind } from 'decko';
e.target.parentNode.removeChild(e.target);
}
function formatCountryLabel(p) {
const src = "/static/img/country-flags/"+ p.Label.toLowerCase() +".png"
return (
<td>
{p.Label}
<img height="12" src={src} class="pull-right" onError={removeImgElement} />
</td>
)
}
class Dashboard extends Component { class Dashboard extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
period: parseInt(window.location.hash.substring(2)) || 7 period: (window.location.hash.substring(2) || 'week'),
} }
this.onPeriodChoose = this.onPeriodChoose.bind(this)
} }
onPeriodChoose(p) { @bind
changePeriod(p) {
this.setState({ period: p }) this.setState({ period: p })
window.history.replaceState(this.state, null, `#!${p}`) window.history.replaceState(this.state, null, `#!${p}`)
} }
render() { render(props, state) {
return ( return (
<div> <div class="rapper">
<HeaderBar showLogout={true} onLogout={this.props.onLogout} />
<div class="container"> <header class="section">
<Realtime /> <nav class="main-nav animated fadeInDown">
<div class="clear"> <ul>
<DatePicker period={this.state.period} onChoose={this.onPeriodChoose} /> <li class="logo"><a href="/">Fathom</a></li>
<li class="visitors"><Realtime /></li>
<li class="spacer">&middot;</li>
<li class="signout"><LogoutButton onSuccess={this.props.onLogout} /></li>
</ul>
</nav>
</header>
<section class="section animated fadeInUp delayed_02s">
<nav class="date-nav">
<DatePicker onChange={this.changePeriod} value={state.period} />
</nav>
<div class="boxes">
<div class="box box-totals animated fadeInUp delayed_03s">
<CountWidget title="Unique visitors" endpoint="visitors" period={state.period} />
<CountWidget title="Page views" endpoint="pageviews" period={state.period} />
<CountWidget title="Avg time on site" endpoint="time-on-site" format="time" period={state.period} />
<CountWidget title="Bounce rate" endpoint="bounce-rate" format="percentage" period={state.period} />
</div>
<Table endpoint="pageviews" headers={["Top pages", "Views", "Uniques"]} />
<Table endpoint="referrers" headers={["Top referrers", "Views", "Uniques"]} />
</div> </div>
<div class="row"> </section>
<div class="col-2">
<CountWidget title="Visitors" endpoint="visitors" period={this.state.period} /> <footer class="section"></footer>
</div> </div>
<div class="col-2">
<CountWidget title="Pageviews" endpoint="pageviews" period={this.state.period} />
</div>
</div>
<GraphWidget period={this.state.period} />
<div class="row">
<div class="col-4">
<Pageviews period={this.state.period} />
</div>
<div class="col-2">
<Table period={this.state.period} endpoint="languages" title="Languages" headers={["#", "Language", "Count", "%"]} />
</div>
</div>
<div class="row">
<div class="col-2">
<Table period={this.state.period} endpoint="screen-resolutions" title="Screen Resolutions" headers={["#", "Resolution", "Count", "%"]} />
</div>
<div class="col-2">
<Table period={this.state.period} endpoint="referrers" title="Referrers" headers={["#", "URL", "Count", "%"]} labelCell={(p) => ( <td><a href={p.Label}>{p.Label.substring(0, 15).replace('https://', '').replace('http://', '')}</a></td>)} />
</div>
<div class="col-2">
<Table period={this.state.period} endpoint="browsers" title="Browsers" headers={["#", "Browser", "Count", "%"]} onAuthError={this.props.onLogout} />
</div>
</div>
</div>
</div>
)} )}
} }

View File

@ -2,13 +2,11 @@
import { h, render, Component } from 'preact'; import { h, render, Component } from 'preact';
import LoginForm from '../components/LoginForm.js'; import LoginForm from '../components/LoginForm.js';
import HeaderBar from '../components/HeaderBar.js';
class Login extends Component { class Login extends Component {
render() { render() {
return ( return (
<div> <div>
<HeaderBar showLogout={false} />
<div class="container"> <div class="container">
<LoginForm onSuccess={this.props.onLogin}/> <LoginForm onSuccess={this.props.onLogin}/>
</div> </div>

View File

@ -1,45 +0,0 @@
label {
font-weight: bold;
display: block;
margin-bottom: 2px;
}
input {
padding: 8px;
border: 1px solid #999;
min-width: 260px;
&:focus {
border: 1px solid #33C3F0;
outline: 0;
}
}
input[type="checkbox"],
input[type="radio"] {
min-width: auto;
vertical-align: middle;
width: 16px;
height: 16px;
}
button,
input[type="submit"],
input[type="button"],
.button {
min-width: auto;
padding: 10px 20px;
cursor: pointer;
border: 0;
color: white;
background: #09F;
font-weight: bold;
&.active,
&:hover,
&:focus {
background: darken( #09F, 20%)
}
}

View File

@ -1,89 +0,0 @@
.d3-tip span {
color: #ff00c7;
}
.domain {
display: none;
}
.axis line {
stroke-width: 1px;
stroke: #eee;
shape-rendering: crispedges;
}
.axis text {
fill: #888;
}
rect {
fill: lighten( #339cff, 10%);
fill-opacity: 0.5;
}
rect:hover {
fill-opacity: 1;
}
.sub-bar rect {
fill: darken(#339cff, 20%);
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
pointer-events: none;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
position: absolute;
pointer-events: none;
}
.d3-tip h5 {
color: #CCC;
}
.d3-tip span {
color: lightgreen;
}
/* Northward tooltips */
.d3-tip.n:after {
content: "\25BC";
margin: -1px 0 0 0;
top: 100%;
left: 0;
text-align: center;
}
/* Eastward tooltips */
.d3-tip.e:after {
content: "\25C0";
margin: -4px 0 0 0;
top: 50%;
left: -8px;
}
/* Southward tooltips */
.d3-tip.s:after {
content: "\25B2";
margin: 0 0 1px 0;
top: -8px;
left: 0;
text-align: center;
}
/* Westward tooltips */
.d3-tip.w:after {
content: "\25B6";
margin: -4px 0 0 -1px;
top: 50%;
left: 100%;
}

View File

@ -1,55 +0,0 @@
$col-width: 100 / 6;
@media( min-width: 680px ) {
.row {
margin-left: -10px;
margin-right: -10px;
&:before,
&:after {
content: " ";
display: table;
}
&:after {
clear: both;
}
}
.col,
.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6 {
float: left;
padding-left: 10px;
padding-right: 10px;
box-sizing: border-box;
}
.col-1 {
width: $col-width + 0%;
}
.col-2 {
width: $col-width * 2 + 0%;
}
.col-3 {
width: $col-width * 3 + 0%;
}
.col-4 {
width: $col-width * 4 + 0%;
}
.col-5 {
width: $col-width * 5 + 0%;
}
.col-6 {
width: 100%;
}
}

View File

@ -1,461 +0,0 @@
/*! 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;
}

View File

@ -1,32 +0,0 @@
table {
width: 100%;
min-width: 100%;
border-collapse: collapse;
tbody > tr {
&:nth-of-type(odd) {
background-color: #FAFAFA;
}
&:hover {
background-color: #F6F6F6;
}
}
thead th {
border-top: 0;
border-bottom: 2px solid #ddd;
}
th {
color: #222;
}
th, td {
text-align: left;
border-top: 1px solid #ddd;
padding: 8px;
}
}

View File

@ -1,76 +0,0 @@
.pull-right {
float: right;
}
.pull-left {
float: left;
}
.cf:before,
.cf:after {
content: " "; /* 1 */
display: table; /* 2 */
}
.cf:after {
clear: both;
}
.tiny-margin{
margin-top: 10px;
margin-bottom: 10px;
}
.small-margin {
margin-top: 20px;
margin-bottom: 20px;
}
.medium-margin {
margin-top: 40px;
margin-bottom: 40px;
}
.no-margin {
margin: 0 !important;
}
.clear {
clear: both;
}
.big {
font-size: 32px;
}
.muted {
color: #888;
}
.italic {
font-style: italic;
}
.center-text {
text-align: center;
}
.positive {
color: limegreen;
&:before {
content: "+";
}
}
.negative {
color: orangered;
}
.inline {
display: inline-block;
}
.small-margin-right {
margin-right: 20px;
}

View File

@ -1,163 +1,114 @@
@import "normalize"; /*
@import "util";
@import "grid"; TODO: move fonts to local hosting - only host the needed weights/styles not all weights styles
@import "forms";
@import "tables"; overpass 200, 500, 600
purple #533feb
green #88ffc6
dark #46494d
medium #98a0a6
light #f5f7fa
padding 8, 16, 20, 32, 64, 128, 256, 512, 1024
font size 12, 16, 64
*/
@import url('//overpass-30e2.kxcdn.com/overpass.css');
* { font: 400 16px/1 'overpass', sans-serif; padding: 0; margin: 0; border: 0; outline: 0; border-radius: 0; border: none; vertical-align: baseline; -webkit-appearance: none; appearance: none; list-style: none; box-sizing: border-box; }
::selection { background: #a0ffd1; }
::-moz-selection { background: #a0ffd1; }
* { @keyframes fadeInUp {
box-sizing: border-box; 0% { opacity: 0; transform: translateY(20px); }
} 100% { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInDown {
0% { opacity: 0; transform: translateY(-20px); }
100% { opacity: 1; transform: translateY(0); }
}
.animated { animation-duration: .4s; animation-fill-mode: both; }
.delayed_02s { animation-delay: .2s; }
.delayed_03s { animation-delay: .3s; }
.delayed_04s { animation-delay: .4s; }
.delayed_05s { animation-delay: .5s; }
.delayed_06s { animation-delay: .6s; }
.fadeInUp { animation-name: fadeInUp; }
.fadeInDown { animation-name: fadeInDown; }
html {}
body { background: #f5f7fa; text-align: center; padding: 8px; }
.rapper { max-width: 1180px; margin: 0 auto; text-align: left; }
.section { margin-bottom: 32px; }
header {}
section {}
footer {}
.boxes { display: flex; margin: 8px 0; flex-wrap: wrap; flex-direction: row; justify-content: flex-start; align-items: stretch; width: 100%; }
.box { border-radius: 4px; margin-bottom: 8px; box-shadow: 0 2px 8px 0 rgba(70,73,77,.16); padding: 24px 0; flex: 1; flex-basis: 100%; }
.box-totals { background: #46494d; color: #fff; padding: 32px 16px 0 16px; }
.box-pages { background: #fff; }
.box-referrers { background: #fff; }
nav.main-nav ul { width: 100%; text-align: right; margin-top: 4px; }
nav li { display: inline-block; }
nav li a { transition: color .2s ease; position: relative; display: inline-block; padding: 0 8px 0 0; }
nav.main-nav li a { padding: 6px 8px 6px 0; }
nav li a:hover { color: #98a0a6; }
nav.date-nav li.active a:after { content:""; background: #88ffc6; display: block; width: 100%; height: 3px; position: absolute; top: 4px; z-index: -1; margin: 0 0 0 -4px; transition: all .4s ease; }
body { nav.date-nav li a { color: #46494d; }
font-family: Raleway, HelveticaNeue, "Helvetica Neue", Helvetica, Arial, sans-serif; nav.date-nav li a:hover { color: #98a0a6; }
font-size: 14px; nav.date-nav li.active a:hover { color: #46494d; }
color: #444;
background: #f5f5f5;
}
h1, h2, h3, h4, h5 { nav li.visitors { color: #533feb; }
color: #111; nav li.signout a { padding-right: 0; }
margin: 0; nav li.logo { float: left; }
nav li.logo a { color: #533feb; display: inline-block; background: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 48 48' xml:space='preserve'%3e%3cpath style='fill:%23533feb;' d='M47.882,26.381C47.96,25.598,48,24.804,48,24c0.001-6.623-2.688-12.632-7.029-16.971 C36.632,2.688,30.623-0.001,24,0C17.377-0.001,11.368,2.688,7.029,7.029C2.688,11.368-0.001,17.377,0,24 c0,3.917,0.941,7.624,2.609,10.892c0,0,0,0,0,0c1.985,3.891,4.998,7.165,8.682,9.47C14.975,46.667,19.338,48.001,24,48 c6.221,0.001,11.901-2.372,16.162-6.258C44.424,37.858,47.284,32.45,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381 C47.882,26.381,47.882,26.381,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381C47.882,26.381,47.882,26.381,47.882,26.381 z M24,2.824c5.852,0.001,11.137,2.368,14.974,6.202c3.596,3.599,5.902,8.472,6.175,13.891l-8.386-8.386 c-0.263-0.263-0.627-0.414-0.998-0.414s-0.735,0.151-0.998,0.413L22.588,26.709l-5.59-5.59c-0.551-0.551-1.445-0.551-1.997,0 l-10.69,10.69C3.353,29.394,2.824,26.762,2.824,24c0.001-5.852,2.368-11.137,6.202-14.974C12.863,5.192,18.148,2.824,24,2.824z'/%3e%3cpath style='fill:%23fff;' d='M4.312,31.809l10.69-10.69c0.551-0.551,1.445-0.551,1.997,0l5.59,5.59l12.178-12.178 c0.263-0.263,0.626-0.413,0.998-0.413s0.735,0.151,0.998,0.414l8.386,8.386c-0.273-5.42-2.579-10.293-6.175-13.891 C35.137,5.192,29.852,2.824,24,2.824C18.148,2.824,12.863,5.192,9.026,9.026C5.192,12.863,2.824,18.148,2.824,24 C2.824,26.762,3.353,29.394,4.312,31.809z'/%3e%3c/svg%3e") top left no-repeat; background-size: 24px 24px; height: 24px; padding: 6px 0 6px 32px; }
small {
font-weight: normal;
font-style: italic; .main-nav ul { display: inline-block; }
color: #666; .spacer { color: #98a0a6; padding: 0 8px; }
}
} svg { width: 24px; height: 24px; display: inline-block; vertical-align: top; }
h5 { .header div, .date-nav a, .total-heading { font-size: 12px; text-transform: uppercase; color: #98a0a6; }
font-size: 15px;
} p, li, .cell { }
.total-numbers { font-size: 44px; letter-spacing: -3px; margin-bottom: 32px; font-weight: 200; }
a { .totals-detail { width: 48%; display: inline-block; }
color: #09f; .total-heading { color: #fff; opacity: .6; }
text-decoration: none;
.table-row { display: flex; flex-direction: row; flex-grow: 0; flex-wrap: wrap; width: 100%; position: relative; margin-bottom: 2px; padding: 0 16px; }
&:hover, .cell { flex-grow: 1; width: 20%; text-align: left; padding: 8px 0; position: relative; z-index: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
&:focus { .main-col { width: 56%; margin-right: 4%; }
text-decoration: underline;
} .header:after { display: none; }
} .table-row:after { content: ""; background: #88ffc6; position: absolute; height: 30px; top: 0; left: 0; opacity: .2; border-right: 2px solid #45ce8c; }
.container {
max-width: 1020px; a { color: #46494d; text-decoration: none; transition: all .4s ease; }
padding: 0 20px; a:hover {}
margin-left: auto; .cell a:hover { color: #533feb; }
margin-right: auto;
} @media ( min-width: 1220px ) {
nav.main-nav ul { margin-top: 24px; }
.header {
margin: 20px 0; .boxes { justify-content: space-between; flex-wrap: nowrap; }
} .box { margin: 0 4px; }
.box-totals { max-width: 230px; margin-left: 0; }
.header-bar { .box-referrers { margin-right: 0; }
background: black;
color: white; .totals-detail { width: 100%; }
margin-bottom: 20px; .total-numbers { font-size: 64px; }
padding: 12px 0; }
line-height: 32px;
font-weight: bold; .w100:after{width:99%}.w99:after{width:99%}.w98:after{width:98%}.w97:after{width:97%}.w96:after{width:96%}.w95:after{width:95%}.w94:after{width:94%}.w93:after{width:93%}.w92:after{width:92%}.w91:after{width:91%}.w90:after{width:90%}.w89:after{width:89%}.w88:after{width:88%}.w87:after{width:87%}.w86:after{width:86%}.w85:after{width:85%}.w84:after{width:84%}.w83:after{width:83%}.w82:after{width:82%}.w81:after{width:81%}.w80:after{width:80%}.w79:after{width:79%}.w78:after{width:78%}.w77:after{width:77%}.w76:after{width:76%}.w75:after{width:75%}.w74:after{width:74%}.w73:after{width:73%}.w72:after{width:72%}.w71:after{width:71%}.w70:after{width:70%}.w69:after{width:69%}.w68:after{width:68%}.w67:after{width:67%}.w66:after{width:66%}.w65:after{width:65%}.w64:after{width:64%}.w63:after{width:63%}.w62:after{width:62%}.w61:after{width:61%}.w60:after{width:60%}.w59:after{width:59%}.w58:after{width:58%}.w57:after{width:57%}.w56:after{width:56%}.w55:after{width:55%}.w54:after{width:54%}.w53:after{width:53%}.w52:after{width:52%}.w51:after{width:51%}.w50:after{width:50%}.w49:after{width:49%}.w48:after{width:48%}.w47:after{width:47%}.w46:after{width:46%}.w45:after{width:45%}.w44:after{width:44%}.w43:after{width:43%}.w42:after{width:42%}.w41:after{width:41%}.w40:after{width:40%}.w39:after{width:39%}.w38:after{width:38%}.w37:after{width:37%}.w36:after{width:36%}.w35:after{width:35%}.w34:after{width:34%}.w33:after{width:33%}.w32:after{width:32%}.w31:after{width:31%}.w30:after{width:30%}.w29:after{width:29%}.w28:after{width:28%}.w27:after{width:27%}.w26:after{width:26%}.w25:after{width:25%}.w24:after{width:24%}.w23:after{width:23%}.w22:after{width:22%}.w21:after{width:21%}.w20:after{width:20%}.w19:after{width:19%}.w18:after{width:18%}.w17:after{width:17%}.w16:after{width:16%}.w15:after{width:15%}.w14:after{width:14%}.w13:after{width:13%}.w12:after{width:12%}.w11:after{width:11%}.w10:after{width:10%}.w09:after{width:9%}.w08:after{width:8%}.w07:after{width:7%}.w06:after{width:6%}.w05:after{width:5%}.w04:after{width:4%}.w03:after{width:3%}.w02:after{width:2%}.w01:after{width:1%}.w00:after{width:0}
h1, h2, h3, a { color: white; }
.subtitle{
color: #BBB;
margin-left: 10px;
font-size: 20px;
}
}
.block {
width: auto;
background: white;
padding: 20px;
margin-bottom: 20px;
position: relative;
h1, h2, h3 {
margin-bottom: 20px;
}
}
.block-float {
margin-right: 20px;
float: left;
}
.loading-overlay {
position: absolute;
background: rgba( 255, 255, 255, 0.9 );
width: 100%;
height: 100%;
z-index: 10;
font-weight: bold;
margin: -20px;
display: flex;
align-items: center;
justify-content: center;
div {
margin: 50px;
height: 28px;
width: 28px;
animation: rotate 0.8s infinite linear;
border: 8px solid #AAA;
border-right-color: transparent;
border-radius: 50%;
}
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.count {
font-weight: bold;
font-size: 120%;
}
.notification {
top: 20px;
left: 0;
right: 0;
text-align: center;
position: fixed;
color: white;
div {
color: white;
display: inline-block;
padding: 6px 12px;
background: green;
}
.notification-error {
background: red;
}
}
h1 {
font-size: 28px;
}
h2 {
font-size: 24px;
}
h3{
font-size: 20px;
}
h4 {
font-size: 16px;
}
small {
font-size: 70%;
}
@import "graphs"