Browse Source

Start the measurements :high_brightness::hotsprings::chart_with_upwards_trend:

:watermelon:
master
Daniel da Silva 3 years ago
commit
3cf0e60008
50 changed files with 3036 additions and 0 deletions
  1. 11
    0
      .build_scripts/deploy.sh
  2. 8
    0
      .build_scripts/lint.sh
  3. 29
    0
      .eslintrc
  4. 1
    0
      .gitattributes
  5. 118
    0
      .gitignore
  6. 1
    0
      .nvmrc
  7. 39
    0
      .travis.yml
  8. 13
    0
      LICENSE
  9. 36
    0
      README.md
  10. 1
    0
      app/CNAME
  11. 7
    0
      app/assets/graphics/collecticons/clock.svg
  12. 1
    0
      app/assets/graphics/layout/devseed-logo-symbol.svg
  13. 1
    0
      app/assets/graphics/layout/ds-logo-neg.svg
  14. 1
    0
      app/assets/graphics/layout/ds-logo-pos.svg
  15. BIN
      app/assets/graphics/meta/apple-touch-icon-114x114.png
  16. BIN
      app/assets/graphics/meta/apple-touch-icon-120x120.png
  17. BIN
      app/assets/graphics/meta/apple-touch-icon-144x144.png
  18. BIN
      app/assets/graphics/meta/apple-touch-icon-152x152.png
  19. BIN
      app/assets/graphics/meta/apple-touch-icon-57x57.png
  20. BIN
      app/assets/graphics/meta/apple-touch-icon-72x72.png
  21. BIN
      app/assets/graphics/meta/apple-touch-icon-76x76.png
  22. BIN
      app/assets/graphics/meta/apple-touch-icon.png
  23. BIN
      app/assets/graphics/meta/default-meta-image.png
  24. BIN
      app/assets/graphics/meta/favicon.ico
  25. 44
    0
      app/assets/scripts/actions/action-creators.js
  26. 15
    0
      app/assets/scripts/actions/action-types.js
  27. 444
    0
      app/assets/scripts/components/charts/chart-line.js
  28. 77
    0
      app/assets/scripts/components/sensor-widget.js
  29. 30
    0
      app/assets/scripts/config.js
  30. 17
    0
      app/assets/scripts/config/production.js
  31. 8
    0
      app/assets/scripts/config/staging.js
  32. 48
    0
      app/assets/scripts/main.js
  33. 40
    0
      app/assets/scripts/reducers/reducer.js
  34. 22
    0
      app/assets/scripts/utils/format.js
  35. 33
    0
      app/assets/scripts/views/app.js
  36. 263
    0
      app/assets/scripts/views/home.js
  37. 29
    0
      app/assets/scripts/views/uhoh.js
  38. 282
    0
      app/assets/styles/_base.scss
  39. 45
    0
      app/assets/styles/_charts.scss
  40. 24
    0
      app/assets/styles/_functions.scss
  41. 143
    0
      app/assets/styles/_mixins.scss
  42. 425
    0
      app/assets/styles/_normalize.scss
  43. 143
    0
      app/assets/styles/_reset.scss
  44. 105
    0
      app/assets/styles/_utils.scss
  45. 70
    0
      app/assets/styles/_variables.scss
  46. 55
    0
      app/assets/styles/main.scss
  47. 63
    0
      app/index.html
  48. BIN
      devseed-sense-dashboard.png
  49. 251
    0
      gulpfile.js
  50. 93
    0
      package.json

+ 11
- 0
.build_scripts/deploy.sh View File

@@ -0,0 +1,11 @@
1
+#!/usr/bin/env bash
2
+set -e # halt script on error
3
+
4
+echo "Get ready, we're pushing to gh-pages!"
5
+cd dist
6
+git init
7
+git config user.name "Travis-CI"
8
+git config user.email "travis-ci@danielfdsilva.com"
9
+git add .
10
+git commit -m "CI deploy to gh-pages"
11
+git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages

+ 8
- 0
.build_scripts/lint.sh View File

@@ -0,0 +1,8 @@
1
+#!/usr/bin/env bash
2
+set -e # halt script on error
3
+# If deploying, don't balk on lint errors
4
+if [ $TRAVIS_PULL_REQUEST = "false" ] && [ $TRAVIS_BRANCH = ${DEPLOY_BRANCH} ]; then
5
+  npm run lint || true
6
+else
7
+  npm run lint
8
+fi

+ 29
- 0
.eslintrc View File

@@ -0,0 +1,29 @@
1
+{
2
+  "extends": ["semistandard"],
3
+  "env": {
4
+    "es6": true,
5
+    "browser": true
6
+  },
7
+  "plugins": [
8
+    "react"
9
+  ],
10
+  "ecmaFeatures": {
11
+    "jsx": true
12
+  },
13
+  rules: {
14
+    'react/display-name': 1 ,
15
+    'react/jsx-no-duplicate-props': 2,
16
+    'react/jsx-no-undef': 2,
17
+    'react/jsx-uses-react': 2,
18
+    'react/jsx-uses-vars': 2,
19
+    'react/no-danger': 0,
20
+    'react/no-deprecated': 2,
21
+    'react/no-did-mount-set-state': [2, 'allow-in-func'],
22
+    'react/no-did-update-set-state': [2, 'allow-in-func'],
23
+    'react/no-direct-mutation-state': 2,
24
+    'react/no-is-mounted': 2,
25
+    'react/no-unknown-property': 2,
26
+    'react/prop-types': 2,
27
+    'react/react-in-jsx-scope': 2
28
+  }
29
+}

+ 1
- 0
.gitattributes View File

@@ -0,0 +1 @@
1
+* text=auto

+ 118
- 0
.gitignore View File

@@ -0,0 +1,118 @@
1
+################################################
2
+###############  .gitignore   ##################
3
+################################################
4
+#
5
+# This file is only relevant if you are using git.
6
+#
7
+# Files which match the splat patterns below will
8
+# be ignored by git.  This keeps random crap and
9
+# and sensitive credentials from being uploaded to 
10
+# your repository.  It allows you to configure your
11
+# app for your machine without accidentally
12
+# committing settings which will smash the local 
13
+# settings of  other developers on your team. 
14
+#
15
+# Some reasonable defaults are included below,
16
+# but, of course, you should modify/extend/prune
17
+# to fit your needs!
18
+################################################
19
+
20
+
21
+
22
+
23
+################################################
24
+# Local Configuration
25
+#
26
+# Explicitly ignore files which contain:
27
+#
28
+# 1. Sensitive information you'd rather not push to
29
+#    your git repository.
30
+#    e.g., your personal API keys or passwords.
31
+#
32
+# 2. Environment-specific configuration
33
+#    Basically, anything that would be annoying
34
+#    to have to change every time you do a 
35
+#    `git pull`
36
+#    e.g., your local development database, or
37
+#    the S3 bucket you're using for file uploads
38
+#    development.
39
+# 
40
+################################################
41
+
42
+app/assets/scripts/config/local.js
43
+
44
+
45
+
46
+
47
+
48
+################################################
49
+# Dependencies
50
+#
51
+# When releasing a production app, you may 
52
+# consider including your node_modules and
53
+# bower_components directory in your git repo,
54
+# but during development, its best to exclude it,
55
+# since different developers may be working on
56
+# different kernels, where dependencies would
57
+# need to be recompiled anyway.
58
+#
59
+# More on that here about node_modules dir:
60
+# http://www.futurealoof.com/posts/nodemodules-in-git.html
61
+# (credit Mikeal Rogers, @mikeal)
62
+#
63
+# About bower_components dir, you can see this:
64
+# http://addyosmani.com/blog/checking-in-front-end-dependencies/
65
+# (credit Addy Osmani, @addyosmani)
66
+# 
67
+################################################
68
+
69
+node_modules
70
+bower_components
71
+.sass-cache
72
+test/bower_components
73
+app/assets/styles/_collecticons.scss
74
+
75
+################################################
76
+# Node.js / NPM
77
+#
78
+# Common files generated by Node, NPM, and the
79
+# related ecosystem.
80
+################################################
81
+
82
+lib-cov
83
+*.seed
84
+*.log
85
+*.out
86
+*.pid
87
+npm-debug.log
88
+
89
+
90
+################################################
91
+# Apidocs
92
+#
93
+# Common files generated by apidocs
94
+################################################
95
+
96
+
97
+
98
+################################################
99
+# Miscellaneous
100
+#
101
+# Common files generated by text editors,
102
+# operating systems, file systems, etc.
103
+################################################
104
+
105
+*~
106
+*#
107
+.DS_STORE
108
+.netbeans
109
+nbproject
110
+.idea
111
+.resources
112
+.node_history
113
+temp
114
+tmp
115
+.tmp
116
+dist
117
+
118
+_README.md

+ 1
- 0
.nvmrc View File

@@ -0,0 +1 @@
1
+v4.2.2

+ 39
- 0
.travis.yml View File

@@ -0,0 +1,39 @@
1
+language: node_js
2
+node_js:
3
+  - "4.2"
4
+
5
+env:
6
+  global:
7
+  - CXX=g++-4.8
8
+  - GH_REF=github.com/developmentseed/sense.git
9
+  - DEPLOY_BRANCH=master
10
+  - secure: "sxDPVseehEH9G/JMuM1E7hsSnGeG4Ju3poWDbp/3Q/Y2Phx/U5IYOQ9e1kggafBi61JOsi8/cySnmiC7c0wP/EHoMnh9gcyAGPdTU4N1UVfZ7xM0eIqm4LQrHBHM0A9ghr3ZhevRIX07LqcNjC0kUR9RX/mlQUvTy/725SzKmQIE/nCWxJwLG5GYRytE/SyX8pzOhuCgIsuBeebpC1Dl91AZmofcym7Lawezvr0ZE1ZcP3wMiZHTCQ8mBIMzd7mSD8rdiZH/QggVPtis5aCSDomOdiKIxxpDM3Mj1J42xtSiEIhRlU9nk3smb9Auioql+BzV5bE8a4oW1D66c320Yb6SAnkSM90a+g0NlS1Z6blNjJ4N8L4QCmVHH4S+AyiXgqccrZiRl/2tsoKE+ZoBFvzIxmOzOQUqGVKX47hQq7QLHFKLCaJIx2XKhHPrfUhi752+5rYFG01teIkOP0usac34qM96dCxOodmC6xUY2YpBWboIwSr3nhQtLBs2fUw23PlgaDdgGpOyv3Pporwnp1Js99UZXtSBDWHcaFxjWnp3gEUdWwMwR69DMOzu6Udm9RMj3WtPm7G5fz9E5pPlVjiBFgsLaUXV3WVNwH/G1zrstl5vFAGyUzI1iJeHTGvncD/HT21uMHNkl6u5VA2+n97W8+J8pvxefQ+Plja7+gM="
11
+  
12
+addons:
13
+  apt:
14
+    sources:
15
+      - ubuntu-toolchain-r-test
16
+    packages:
17
+      - g++-4.8
18
+
19
+cache:
20
+  apt: true
21
+  directories:
22
+    - node_modules
23
+
24
+before_install:
25
+- chmod +x ./.build_scripts/deploy.sh
26
+- chmod +x ./.build_scripts/lint.sh
27
+
28
+before_script:
29
+- ./.build_scripts/lint.sh
30
+
31
+script:
32
+- npm run build
33
+
34
+deploy:
35
+  provider: script
36
+  skip_cleanup: true
37
+  script: .build_scripts/deploy.sh
38
+  on:
39
+    branch: ${DEPLOY_BRANCH}

+ 13
- 0
LICENSE View File

@@ -0,0 +1,13 @@
1
+        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
2
+                    Version 2, December 2004 
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> 
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified 
7
+ copies of this license document, and changing it is allowed as long 
8
+ as the name is changed. 
9
+
10
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
11
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 
12
+
13
+  0. You just DO WHAT THE FUCK YOU WANT TO.

+ 36
- 0
README.md View File

@@ -0,0 +1,36 @@
1
+# Devseed Sense
2
+
3
+Simple dashboard that taps into the [opensensemap](http://opensensemap.org/) api to show the measurements for a specific [sensebox](www.sensebox.de/en/).
4
+
5
+![devseed-sense-dashboard.png](devseed-sense-dashboard.png)
6
+
7
+## Development environment
8
+To set up the development environment for this website, you'll need to install the following on your system:
9
+
10
+- Node (v4.2.x) & Npm ([nvm](https://github.com/creationix/nvm) usage is advised)
11
+
12
+> The versions mentioned are the ones used during development. It could work with newer ones.
13
+  Run `nvm use` to activate the correct version.
14
+
15
+After these basic requirements are met, run the following commands in the website's folder:
16
+```
17
+$ npm install
18
+```
19
+
20
+### Getting started
21
+
22
+```
23
+$ npm run serve
24
+```
25
+Compiles the sass files, javascript, and launches the server making the site available at `http://localhost:1337/`
26
+The system will watch files and execute tasks whenever one of them changes.
27
+The site will automatically refresh since it is bundled with livereload.  
28
+  
29
+The current code will show the values for the [DS Lisbon sensebox](http://opensensemap.org/#/explore/570629b945fd40c8197462fb).  
30
+This can be changed by setting the correct ids in `config/production.js`
31
+
32
+### Other commands
33
+Compile the files to the `dist` folder ready for production.
34
+```
35
+$ npm run build
36
+```

+ 1
- 0
app/CNAME View File

@@ -0,0 +1 @@
1
+sense.devseed.com

+ 7
- 0
app/assets/graphics/collecticons/clock.svg View File

@@ -0,0 +1,7 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
5
+
6
+<path d="M288,128h-64v141.3l105.4,105.3l45.2-45.3L288,242.7V128z M256,0C114.6,0,0,114.6,0,256s114.6,256,256,256  s256-114.6,256-256S397.4,0,256,0z M256,448c-106,0-192-86-192-192S150,64,256,64s192,86,192,192S362,448,256,448z"/>
7
+</svg>

+ 1
- 0
app/assets/graphics/layout/devseed-logo-symbol.svg View File

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><path fill="#D04003" d="M512 32c0-17.7-14.3-32-32-32h-448c-17.7 0-32 14.3-32 32v448c0 17.7 14.3 32 32 32h448c17.7 0 32-14.3 32-32v-448z"/><path fill="#fff" d="M289.5 434.7c-.1-10.3 2.6-19.2 7-28.5-9.2 8.1-12.8 17.3-16.2 29.8.9-14.6 8-110.4 8-115.2 24.5-70 13.6-279 44.8-305.1-36 6.5-48.2 43.3-59.7 72.5-11.4-34.3-2.4-56.1-40.2-68.1 30.2 25.6 24.2 47.6 30.4 83-18.1-23.5-12.5-52.8-33.3-76.8-5.9-6.1-25.5-21-25.5-21 0 .9 15.7 32.3 18.6 48.9 2.9 16.6 9.8 49.8 8.8 62 2.6 67.8 19.6 133.5 31.3 200 2 11.4 7.8 44.5 7.8 44.5l1.3 13.8c-2.1-9.1-4.1-17.3-5.2-20.8-3.4-10-11.8-35.4-13.7-45.8-28.5-58.6-80.6-173.5-150.2-190.4 39.5 35 82 81.2 91.5 125.8-21.2-29.8-104.6-93.4-138.5-116.6 63.9 66.3 115.2 135 170.8 208.7 16.2 21 36.3 63.3 37.7 72.9 1.1 7.1 2.4 33.6 3.2 49.9-31.1-75.1-48.1-113.2-105.5-146.8-9.8-5.2-40.2-15.7-40.2-15.7 13.8 15.2 30.7 16.1 43.3 37.3-26.1-17.1-53.2-20.1-83-21.1 95.7 49.5 147.5 63.3 179.7 158.4 2.6 15.6-1.4 27.8-1 41.8h21.2c-.1-10 .1-19.6 0-21.6 1.1-6.6 1.3-26.6 4.2-32.7 10-25.4 26.2-37.4 48.8-52.2-20.3 6.6-30.6 4-46.2 29.1zm94.5-224.2c-7.8 7.4-31.3 37.2-31.3 37.2 1.3-42 11.9-87 38.7-118.4-37.3 24.4-58.3 84.6-68 121.4-3.8 14.8-47.9 100.7-27.4 146.7 4-16.8 13.5-33.1 24-47.6 4.9-6.6 25.5-39.3 46-86 18.1-47 47.8-74.5 89.6-97.8-31.6 4.8-49.6 23-71.6 44.5zm7.3-81.2z"/></svg>

+ 1
- 0
app/assets/graphics/layout/ds-logo-neg.svg
File diff suppressed because it is too large
View File


+ 1
- 0
app/assets/graphics/layout/ds-logo-pos.svg
File diff suppressed because it is too large
View File


BIN
app/assets/graphics/meta/apple-touch-icon-114x114.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-120x120.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-144x144.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-152x152.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-57x57.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-72x72.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-76x76.png View File


BIN
app/assets/graphics/meta/apple-touch-icon.png View File


BIN
app/assets/graphics/meta/default-meta-image.png View File


BIN
app/assets/graphics/meta/favicon.ico View File


+ 44
- 0
app/assets/scripts/actions/action-creators.js View File

@@ -0,0 +1,44 @@
1
+import fetch from 'isomorphic-fetch';
2
+import * as actions from './action-types';
3
+import config from '../config';
4
+
5
+// //////////////////////////////////////////////////////////////////////////
6
+// // Fetch Section Access Thunk
7
+
8
+function requestSensorData (sensor) {
9
+  return {
10
+    type: actions[`REQUEST_SENSOR_DATA_${sensor.toUpperCase()}`]
11
+  };
12
+}
13
+
14
+function receiveSensorData (sensor, json) {
15
+  return {
16
+    type: actions[`RECEIVE_SENSOR_DATA_${sensor.toUpperCase()}`],
17
+    data: json,
18
+    receivedAt: Date.now()
19
+  };
20
+}
21
+
22
+export function fetchSensorData (sensor, toDate) {
23
+  return dispatch => {
24
+    dispatch(requestSensorData(sensor));
25
+
26
+    let sensorId = config.senseBox[`sensorId--${sensor}`];
27
+    return fetch(`${config.api}/boxes/${config.senseBox.id}/data/${sensorId}?from-date=${toDate}`)
28
+      .then(response => {
29
+        if (response.status >= 400) {
30
+          throw new Error('Bad response');
31
+        }
32
+        return response.json();
33
+      })
34
+      .then(json => {
35
+        dispatch(receiveSensorData(sensor, json));
36
+        // setTimeout(() => {
37
+        //   dispatch(receiveSensorData(sensor, json));
38
+        // }, Math.ceil(Math.random() * 5000));
39
+      }, e => {
40
+        console.log('e', e);
41
+        return dispatch(receiveSensorData(null, null, 'Data not available'));
42
+      });
43
+  };
44
+}

+ 15
- 0
app/assets/scripts/actions/action-types.js View File

@@ -0,0 +1,15 @@
1
+'use strict';
2
+export const REQUEST_SENSOR_DATA_TEMPERATURE = 'REQUEST_SENSOR_DATA_TEMPERATURE';
3
+export const RECEIVE_SENSOR_DATA_TEMPERATURE = 'RECEIVE_SENSOR_DATA_TEMPERATURE';
4
+
5
+export const REQUEST_SENSOR_DATA_PRESSURE = 'REQUEST_SENSOR_DATA_PRESSURE';
6
+export const RECEIVE_SENSOR_DATA_PRESSURE = 'RECEIVE_SENSOR_DATA_PRESSURE';
7
+
8
+export const REQUEST_SENSOR_DATA_LUMINOSITY = 'REQUEST_SENSOR_DATA_LUMINOSITY';
9
+export const RECEIVE_SENSOR_DATA_LUMINOSITY = 'RECEIVE_SENSOR_DATA_LUMINOSITY';
10
+
11
+export const REQUEST_SENSOR_DATA_UV = 'REQUEST_SENSOR_DATA_UV';
12
+export const RECEIVE_SENSOR_DATA_UV = 'RECEIVE_SENSOR_DATA_UV';
13
+
14
+export const REQUEST_SENSOR_DATA_HUMIDITY = 'REQUEST_SENSOR_DATA_HUMIDITY';
15
+export const RECEIVE_SENSOR_DATA_HUMIDITY = 'RECEIVE_SENSOR_DATA_HUMIDITY';

+ 444
- 0
app/assets/scripts/components/charts/chart-line.js View File

@@ -0,0 +1,444 @@
1
+'use strict';
2
+import React from 'react';
3
+import d3 from 'd3';
4
+import _ from 'lodash';
5
+// import Popover from '../../utils/popover';
6
+
7
+var LineChart = React.createClass({
8
+  displayName: 'LineChart',
9
+
10
+  propTypes: {
11
+    className: React.PropTypes.string,
12
+    data: React.PropTypes.array,
13
+    axisLineVal: React.PropTypes.number,
14
+    axisLineMax: React.PropTypes.number,
15
+    axisLineMin: React.PropTypes.number,
16
+    dataUnitSuffix: React.PropTypes.string
17
+  },
18
+
19
+  chart: null,
20
+
21
+  onWindowResize: function () {
22
+    this.chart.checkSize();
23
+  },
24
+
25
+  componentDidMount: function () {
26
+    // console.log('LineChart componentDidMount');
27
+    // Debounce event.
28
+    this.onWindowResize = _.debounce(this.onWindowResize, 200);
29
+
30
+    window.addEventListener('resize', this.onWindowResize);
31
+    this.chart = Chart();
32
+    d3.select(this.refs.container).call(this.chart
33
+      .data(this.props.data)
34
+      .axisLineVal(this.props.axisLineVal)
35
+      .axisValueMax(this.props.axisLineMax)
36
+      .axisValueMin(this.props.axisLineMin)
37
+      .dataUnitSuffix(this.props.dataUnitSuffix));
38
+  },
39
+
40
+  componentWillUnmount: function () {
41
+    // console.log('LineChart componentWillUnmount');
42
+    window.removeEventListener('resize', this.onWindowResize);
43
+    this.chart.destroy();
44
+  },
45
+
46
+  componentDidUpdate: function (prevProps/* prevState */) {
47
+    console.log('LineChart componentDidUpdate');
48
+    this.chart.pauseUpdate();
49
+    if (prevProps.data !== this.props.data) {
50
+      this.chart.data(this.props.data);
51
+    }
52
+    if (prevProps.axisLineVal !== this.props.axisLineVal) {
53
+      this.chart.axisLineVal(this.props.axisLineVal);
54
+    }
55
+    if (prevProps.axisLineMax !== this.props.axisLineMax) {
56
+      this.chart.axisValueMax(this.props.axisLineMax);
57
+    }
58
+    if (prevProps.axisLineMin !== this.props.axisLineMin) {
59
+      this.chart.axisValueMin(this.props.axisLineMin);
60
+    }
61
+    if (prevProps.dataUnitSuffix !== this.props.dataUnitSuffix) {
62
+      this.chart.dataUnitSuffix(this.props.dataUnitSuffix);
63
+    }
64
+    this.chart.continueUpdate();
65
+  },
66
+
67
+  render: function () {
68
+    return (
69
+      <div className={this.props.className} ref='container'></div>
70
+    );
71
+  }
72
+});
73
+
74
+module.exports = LineChart;
75
+
76
+var Chart = function (options) {
77
+  // Data related variables for which we have getters and setters.
78
+  var _data = null;
79
+  var _axisLineVal, _axisValueMin, _axisValueMax, _dataUnitSuffix;
80
+
81
+  // Pause
82
+  var _pauseUpdate = false;
83
+
84
+  // Containters
85
+  var $el, $svg;
86
+  // Var declaration.
87
+  var margin = {top: 16, right: 32, bottom: 32, left: 24};
88
+  var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
89
+  // width and height refer to the data canvas. To know the svg size the margins
90
+  // must be added.
91
+  var _width, _height;
92
+  // Draw functions.
93
+  var line;
94
+
95
+  // Update functions.
96
+  var updateData, upateSize;
97
+
98
+  // X scale. Range updated in function.
99
+  var x = d3.time.scale();
100
+
101
+  // Y scale. Range updated in function.
102
+  var y = d3.scale.linear();
103
+
104
+  // Used for the zoom translate bounds.
105
+  var minX;
106
+
107
+  // Zoom
108
+  var zoom = d3.behavior
109
+    .zoom()
110
+    .scaleExtent([1, 1]);
111
+
112
+  // Line function for the delimit the area.
113
+  line = d3.svg.line()
114
+    .x(d => x(d.timestep))
115
+    .y(d => y(d.value));
116
+
117
+  // Define xAxis function.
118
+  var xAxis = d3.svg.axis()
119
+    .scale(x)
120
+    .orient('bottom')
121
+    .tickSize(0)
122
+    .tickFormat(d3.time.format('%H:%M'));
123
+    // .ticks(3);
124
+
125
+  function _calcSize () {
126
+    _width = parseInt($el.style('width'), 10) - margin.left - margin.right;
127
+    _height = parseInt($el.style('height'), 10) - margin.top - margin.bottom;
128
+  }
129
+
130
+  function chartFn (selection) {
131
+    $el = selection;
132
+
133
+    var layers = {
134
+      line: function () {
135
+        // lines.
136
+        let the_line = $dataCanvas.selectAll('.data-line')
137
+          .data([_data]);
138
+
139
+        // Handle new.
140
+        the_line.enter()
141
+          .append('path')
142
+          .attr('clip-path', 'url(#clip)');
143
+
144
+        // Update current.
145
+        the_line
146
+            .attr('d', d => line(d))
147
+            .attr('class', d => `data-line`);
148
+
149
+        // Remove old.
150
+        the_line.exit()
151
+          .remove();
152
+      },
153
+
154
+      minMax: function () {
155
+        let [sDate, eDate] = x.domain();
156
+
157
+        let f = (o) => {
158
+          let timestamp = o.timestep.getTime();
159
+          return timestamp >= sDate.getTime() && timestamp <= eDate.getTime();
160
+        };
161
+
162
+        // Min Max.
163
+        let sorted = _(_data).filter(f).sortBy('value').value();
164
+        if (!sorted.length) {
165
+          return;
166
+        }
167
+
168
+        let min = sorted[0];
169
+        let max = _.last(sorted);
170
+
171
+        let edgeG = $dataCanvas.selectAll('.edges')
172
+          .data([0])
173
+          .enter().append('g')
174
+          .attr('class', 'edges');
175
+
176
+        edgeG.append('text')
177
+          .attr('text-anchor', 'middle')
178
+          .attr('dy', '-0.25em')
179
+          .attr('class', 'edge edge-max');
180
+
181
+        edgeG.append('text')
182
+            .attr('text-anchor', 'middle')
183
+            .attr('dy', '1em')
184
+            .attr('class', 'edge edge-min');
185
+
186
+        $dataCanvas.select('.edge.edge-max')
187
+          .datum(max)
188
+          .attr('x', d => x(d.timestep))
189
+          .attr('y', d => y(d.value))
190
+          .text(d => d.value + _dataUnitSuffix);
191
+
192
+        $dataCanvas.select('.edge.edge-min')
193
+          .datum(min)
194
+          .attr('x', d => x(d.timestep))
195
+          .attr('y', d => y(d.value))
196
+          .text(d => d.value + _dataUnitSuffix);
197
+      },
198
+
199
+      xAxis: function () {
200
+        // Append Axis.
201
+        // X axis.
202
+        let xAx = $svg.selectAll('.x.axis')
203
+          .data([0]);
204
+
205
+        xAx.enter().append('g')
206
+          .attr('class', 'x axis')
207
+          .append('text')
208
+          .attr('class', 'label')
209
+          .attr('text-anchor', 'start');
210
+
211
+        xAx
212
+          .attr('transform', `translate(${margin.left},${_height + margin.top + 16})`)
213
+          .call(xAxis);
214
+      },
215
+
216
+      yAxis: function () {
217
+        // Append Axis.
218
+        // Y axis
219
+        let yAx = $svg.selectAll('.y.axis')
220
+          .data([0]);
221
+
222
+        let yAxEnter = yAx.enter().append('g')
223
+          .attr('class', 'y axis');
224
+
225
+        yAxEnter.append('text')
226
+          .attr('class', 'label')
227
+          .attr('text-anchor', 'end');
228
+
229
+        yAxEnter.append('line')
230
+          .attr('class', 'line');
231
+
232
+        yAx.select('.label')
233
+          .attr('y', y(_axisLineVal) + margin.top)
234
+          .attr('x', _width + margin.left + margin.right)
235
+          .attr('dy', '1em')
236
+          .text(_axisLineVal + _dataUnitSuffix);
237
+
238
+        yAx.select('.line')
239
+          .attr('x1', 0)
240
+          .attr('y1', y(_axisLineVal) + margin.top)
241
+          .attr('x2', _width + margin.left + margin.right)
242
+          .attr('y2', y(_axisLineVal) + margin.top);
243
+      },
244
+
245
+      days: function () {
246
+        // Compute days for the days steps.
247
+        let dateCopyDay = d => {
248
+          let n = new Date(d.getTime());
249
+          n.setHours(0);
250
+          n.setMinutes(0);
251
+          n.setSeconds(0);
252
+          n.setMilliseconds(0);
253
+          return n;
254
+        };
255
+        let eDay = dateCopyDay(_.last(_data).timestep);
256
+        let dt = dateCopyDay(_data[0].timestep);
257
+        let daySteps = [dt];
258
+        while (true) {
259
+          dt = d3.time.day.offset(dateCopyDay(dt), 1);
260
+          daySteps.push(dt);
261
+          if (dt.getTime() >= eDay.getTime()) {
262
+            break;
263
+          }
264
+        }
265
+
266
+        let selection = $dataCanvas.selectAll('.days')
267
+          .data([0]);
268
+
269
+        selection.enter().append('g')
270
+          .attr('class', 'days');
271
+
272
+        let $days = selection.selectAll('.day-tick')
273
+          .data(daySteps);
274
+
275
+        $days.enter()
276
+          .append('text')
277
+          .attr('text-anchor', 'middle')
278
+          .attr('class', 'day-tick');
279
+
280
+        $days
281
+          .attr('x', d => x(d))
282
+          .attr('y', _height + margin.top)
283
+          .text(d => `${d.getDate()} ${months[d.getMonth()]}`);
284
+      }
285
+    };
286
+
287
+    upateSize = function () {
288
+      $svg
289
+        .attr('width', _width + margin.left + margin.right)
290
+        .attr('height', _height + margin.top + margin.bottom);
291
+
292
+      $dataCanvas
293
+        .attr('width', _width)
294
+        .attr('height', _height);
295
+
296
+      $svg.select('#clip rect')
297
+        .attr('width', _width + margin.left)
298
+        .attr('height', _height);
299
+
300
+      // Update scale ranges.
301
+      x.range([0, _width]);
302
+      y.range([_height, 0]);
303
+
304
+      // Recalculate the minX and zoom since scale changed.
305
+      minX = x(_data[0].timestep);
306
+      zoom.x(x);
307
+
308
+      // Redraw.
309
+      layers.line();
310
+      layers.minMax();
311
+      layers.days();
312
+      layers.xAxis();
313
+      layers.yAxis();
314
+    };
315
+
316
+    updateData = function () {
317
+      if (!_data || _pauseUpdate) {
318
+        return;
319
+      }
320
+
321
+      // Update scale domains.
322
+      let eDate = _.last(_data).timestep;
323
+      let sDate = d3.time.day.offset(eDate, -1);
324
+      x.domain([sDate, eDate]);
325
+
326
+      // Since the data is stacked the last element will contain the
327
+      // highest values)
328
+      y.domain([_axisValueMin, _axisValueMax]);
329
+
330
+      // Recalculate the minX and zoom since scale changed.
331
+      minX = x(_data[0].timestep);
332
+      zoom.x(x);
333
+
334
+      // Redraw.
335
+      layers.line();
336
+      layers.minMax();
337
+      layers.days();
338
+      layers.xAxis();
339
+      layers.yAxis();
340
+    };
341
+
342
+    // -----------------------------------------------------------------
343
+    // INIT.
344
+    $svg = $el.append('svg')
345
+      .attr('class', 'chart')
346
+      .style('display', 'block');
347
+
348
+    // Datacanvas
349
+    var $dataCanvas = $svg.append('g')
350
+      .attr('class', 'data-canvas')
351
+      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
352
+
353
+    $svg.append('defs')
354
+      .append('clipPath')
355
+      .attr('id', 'clip')
356
+      .append('rect')
357
+      .attr('x', -margin.left) // Compensate for the dataCanvas translate.
358
+      .attr('y', 0);
359
+
360
+    $svg
361
+      .attr('cursor', 'move')
362
+      .call(zoom)
363
+      .on('mousewheel.zoom', null)
364
+      .on('DOMMouseScroll.zoom', null);
365
+
366
+    zoom.on('zoom', function () {
367
+      // Bound translate.
368
+      let [tx, ty] = zoom.translate();
369
+      tx = Math.max(tx, 0);
370
+      tx = Math.min(tx, Math.abs(minX) - margin.right);
371
+      tx = Math.round(tx);
372
+      zoom.translate([tx, ty]);
373
+
374
+      layers.line();
375
+      layers.minMax();
376
+      layers.days();
377
+      layers.xAxis();
378
+    });
379
+
380
+    _calcSize();
381
+    upateSize();
382
+    updateData();
383
+  }
384
+
385
+  chartFn.checkSize = function () {
386
+    _calcSize();
387
+    upateSize();
388
+    return chartFn;
389
+  };
390
+
391
+  chartFn.destroy = function () {
392
+    // Cleanup.
393
+  };
394
+
395
+  // --------------------------------------------
396
+  // Getters and setters.
397
+  chartFn.data = function (d) {
398
+    if (!arguments.length) return _data;
399
+    _data = _.cloneDeep(d);
400
+    if (typeof updateData === 'function') updateData();
401
+    return chartFn;
402
+  };
403
+
404
+  chartFn.axisLineVal = function (d) {
405
+    if (!arguments.length) return _axisLineVal;
406
+    _axisLineVal = d;
407
+    if (typeof updateData === 'function') updateData();
408
+    return chartFn;
409
+  };
410
+
411
+  chartFn.axisValueMin = function (d) {
412
+    if (!arguments.length) return _axisValueMin;
413
+    _axisValueMin = d;
414
+    if (typeof updateData === 'function') updateData();
415
+    return chartFn;
416
+  };
417
+
418
+  chartFn.axisValueMax = function (d) {
419
+    if (!arguments.length) return _axisValueMax;
420
+    _axisValueMax = d;
421
+    if (typeof updateData === 'function') updateData();
422
+    return chartFn;
423
+  };
424
+
425
+  chartFn.dataUnitSuffix = function (d) {
426
+    if (!arguments.length) return _dataUnitSuffix;
427
+    _dataUnitSuffix = d;
428
+    if (typeof updateData === 'function') updateData();
429
+    return chartFn;
430
+  };
431
+
432
+  chartFn.pauseUpdate = function () {
433
+    _pauseUpdate = true;
434
+    return chartFn;
435
+  };
436
+
437
+  chartFn.continueUpdate = function () {
438
+    _pauseUpdate = false;
439
+    if (typeof updateData === 'function') updateData();
440
+    return chartFn;
441
+  };
442
+
443
+  return chartFn;
444
+};

+ 77
- 0
app/assets/scripts/components/sensor-widget.js View File

@@ -0,0 +1,77 @@
1
+'use strict';
2
+import React from 'react';
3
+import ChartLine from './charts/chart-line';
4
+import { numDisplay, formatDate } from '../utils/format';
5
+
6
+var SensorWidget = React.createClass({
7
+  displayName: 'SensorWidget',
8
+
9
+  propTypes: {
10
+    fetching: React.PropTypes.bool,
11
+    fetched: React.PropTypes.bool,
12
+    className: React.PropTypes.string,
13
+    title: React.PropTypes.string,
14
+    lastReading: React.PropTypes.object,
15
+    avgs: React.PropTypes.object,
16
+    plotData: React.PropTypes.array,
17
+    axisLineVal: React.PropTypes.number,
18
+    axisLineMax: React.PropTypes.number,
19
+    axisLineMin: React.PropTypes.number,
20
+    unit: React.PropTypes.string
21
+  },
22
+
23
+  render: function () {
24
+    let {
25
+      className,
26
+      fetching, fetched,
27
+      title,
28
+      lastReading, avgs,
29
+      plotData,
30
+      axisLineVal, axisLineMax, axisLineMin,
31
+      unit } = this.props;
32
+
33
+    if (!fetched && !fetching) {
34
+      return null;
35
+    }
36
+
37
+    return (
38
+      <article className={'card ' + className}>
39
+        <header className='card__header'>
40
+          <div className='card__headline'>
41
+            <h1 className='card__title'>{title} {fetching ? '...' : null}</h1>
42
+            <dl className='stats'>
43
+              <dd className='stats__label'>Last update</dd>
44
+              <dt className='stats__date'>{lastReading !== null ? formatDate(lastReading.timestep) : '--'}</dt>
45
+              <dd className='stats__label'>Current temperature</dd>
46
+              <dt className='stats__value'>{lastReading !== null ? numDisplay(lastReading.value, 1) : '--'}{unit}</dt>
47
+            </dl>
48
+          </div>
49
+        </header>
50
+        <div className='card__body'>
51
+          <div className='infographic'>
52
+            {plotData.length ? (
53
+            <div className='line-chart-wrapper'>
54
+              <ChartLine
55
+                className='line-chart'
56
+                axisLineVal={axisLineVal}
57
+                axisLineMax={axisLineMax}
58
+                axisLineMin={axisLineMin}
59
+                dataUnitSuffix={unit}
60
+                data={plotData} />
61
+            </div>
62
+            ) : null}
63
+            {!plotData.length && fetching ? <p className='card__loading'>Loading Data...</p> : null}
64
+          </div>
65
+          <div className='metrics'>
66
+            <ul className='metrics__list'>
67
+              <li><strong>{avgs !== null ? numDisplay(avgs.today, 1, unit) : '--'}</strong> avg today</li>
68
+              <li><strong>{avgs !== null ? numDisplay(avgs.yesterday, 1, unit) : '--'}</strong> avg yesterday</li>
69
+            </ul>
70
+          </div>
71
+        </div>
72
+      </article>
73
+    );
74
+  }
75
+});
76
+
77
+module.exports = SensorWidget;

+ 30
- 0
app/assets/scripts/config.js View File

@@ -0,0 +1,30 @@
1
+'use strict';
2
+var _ = require('lodash');
3
+/*
4
+ * App configuration.
5
+ *
6
+ * Uses settings in config/production.js, with any properties set by
7
+ * config/staging.js or config/local.js overriding them depending upon the
8
+ * environment.
9
+ *
10
+ * This file should not be modified.  Instead, modify one of:
11
+ *
12
+ *  - config/production.js
13
+ *      Production settings (base).
14
+ *  - config/staging.js
15
+ *      Overrides to production if ENV is staging.
16
+ *  - config/local.js
17
+ *      Overrides if local.js exists.
18
+ *      This last file is gitignored, so you can safely change it without
19
+ *      polluting the repo.
20
+ */
21
+
22
+var configurations = require('./config/*.js', {mode: 'hash'});
23
+var config = configurations.local || {};
24
+
25
+if (process.env.DS_ENV === 'staging') {
26
+  _.defaultsDeep(config, configurations.staging);
27
+}
28
+_.defaultsDeep(config, configurations.production);
29
+
30
+module.exports = config;

+ 17
- 0
app/assets/scripts/config/production.js View File

@@ -0,0 +1,17 @@
1
+'use strict';
2
+/*
3
+ * App config for production.
4
+ */
5
+module.exports = {
6
+  environment: 'production',
7
+  api: 'http://opensensemap.org:8000',
8
+  senseBox: {
9
+    id: '570629b945fd40c8197462fb',
10
+    'sensorId--uv': '570629b945fd40c8197462fd',
11
+    'sensorId--luminosity': '570629b945fd40c8197462fe',
12
+    'sensorId--pressure': '570629b945fd40c8197462ff',
13
+    'sensorId--humidity': '570629b945fd40c819746300',
14
+    'sensorId--temperature': '570629b945fd40c819746301'
15
+  }
16
+};
17
+

+ 8
- 0
app/assets/scripts/config/staging.js View File

@@ -0,0 +1,8 @@
1
+'use strict';
2
+/*
3
+ * App config overrides for staging.
4
+ */
5
+
6
+module.exports = {
7
+  environment: 'staging'
8
+};

+ 48
- 0
app/assets/scripts/main.js View File

@@ -0,0 +1,48 @@
1
+'use strict';
2
+import 'babel-polyfill';
3
+import React from 'react';
4
+import { render } from 'react-dom';
5
+import { Provider } from 'react-redux';
6
+import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
7
+import { createHashHistory } from 'history';
8
+import { createStore, applyMiddleware, compose } from 'redux';
9
+import thunkMiddleware from 'redux-thunk';
10
+import { syncHistory } from 'react-router-redux';
11
+import reducer from './reducers/reducer';
12
+
13
+// import UhOh from './views/uhoh';
14
+import App from './views/app';
15
+import Home from './views/home';
16
+
17
+const appHistory = useRouterHistory(createHashHistory)({ queryKey: false });
18
+
19
+// Sync dispatched route actions to the history
20
+const reduxRouterMiddleware = syncHistory(appHistory);
21
+const finalCreateStore = compose(
22
+  applyMiddleware(reduxRouterMiddleware, thunkMiddleware)
23
+)(createStore);
24
+
25
+const store = finalCreateStore(reducer);
26
+
27
+render((
28
+  <Provider store={store}>
29
+    <Router history={appHistory}>
30
+      <Route path='*' component={App}>
31
+        <IndexRoute component={Home}/>
32
+      </Route>
33
+    </Router>
34
+  </Provider>
35
+), document.querySelector('#site-canvas'));
36
+
37
+// render((
38
+//   <Provider store={store}>
39
+//     <Router history={appHistory}>
40
+//       <Route path='/' component={App}>
41
+//         <IndexRoute component={Home}/>
42
+//       </Route>
43
+//       <Route path='*' component={App}>
44
+//         <IndexRoute component={UhOh}/>
45
+//       </Route>
46
+//     </Router>
47
+//   </Provider>
48
+// ), document.querySelector('#site-canvas'));

+ 40
- 0
app/assets/scripts/reducers/reducer.js View File

@@ -0,0 +1,40 @@
1
+import _ from 'lodash';
2
+import { combineReducers } from 'redux';
3
+import { routeReducer } from 'react-router-redux';
4
+import * as actions from '../actions/action-types';
5
+
6
+const sensorReducerFactory = function (sensor) {
7
+  return function (state = {fetching: false, fetched: false, data: null}, action) {
8
+    let s = sensor.toUpperCase();
9
+    switch (action.type) {
10
+      case actions[`REQUEST_SENSOR_DATA_${s}`]:
11
+        console.log(`REQUEST_SENSOR_DATA_${s}`);
12
+        state = _.cloneDeep(state);
13
+        state.fetching = true;
14
+        break;
15
+      case actions[`RECEIVE_SENSOR_DATA_${s}`]:
16
+        console.log(`RECEIVE_SENSOR_DATA_${s}`);
17
+        state = _.cloneDeep(state);
18
+        state.data = action.data;
19
+        state.fetching = false;
20
+        state.fetched = true;
21
+        break;
22
+    }
23
+    return state;
24
+  };
25
+};
26
+
27
+const sensorUv = sensorReducerFactory('uv');
28
+const sensorLuminosity = sensorReducerFactory('luminosity');
29
+const sensorPressure = sensorReducerFactory('pressure');
30
+const sensorHumidity = sensorReducerFactory('humidity');
31
+const sensorTemperature = sensorReducerFactory('temperature');
32
+
33
+export default combineReducers({
34
+  routing: routeReducer,
35
+  sensorUv,
36
+  sensorLuminosity,
37
+  sensorPressure,
38
+  sensorHumidity,
39
+  sensorTemperature
40
+});

+ 22
- 0
app/assets/scripts/utils/format.js View File

@@ -0,0 +1,22 @@
1
+'use strict';
2
+module.exports.numDisplay = function (n, dec = 2, suffix = '', nan = '--') {
3
+  if (isNaN(n)) {
4
+    return nan;
5
+  }
6
+  let s = n.toString();
7
+  s = (s.indexOf('.') === -1) ? s : s.substr(0, s.indexOf('.') + dec + 1);
8
+  return s + suffix;
9
+};
10
+
11
+module.exports.formatDate = function (date) {
12
+  let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
13
+  let hour = date.getHours();
14
+  hour = hour < 10 ? `0${hour}` : hour;
15
+  let minute = date.getMinutes();
16
+  minute = minute < 10 ? `0${minute}` : minute;
17
+  return `${months[date.getMonth()]} ${date.getDate()}, ${hour}:${minute}`;
18
+};
19
+
20
+module.exports.round = function (n, dec = 2) {
21
+  return +module.exports.numDisplay(n, dec);
22
+};

+ 33
- 0
app/assets/scripts/views/app.js View File

@@ -0,0 +1,33 @@
1
+'use strict';
2
+import React from 'react';
3
+
4
+var App = React.createClass({
5
+  displayName: 'App',
6
+
7
+  propTypes: {
8
+    dispatch: React.PropTypes.func,
9
+    children: React.PropTypes.object
10
+  },
11
+
12
+  render: function () {
13
+    return (
14
+      <div>
15
+        <header className='site-header' role='banner'>
16
+          <div className='inner'>
17
+            <div className='site-headline'>
18
+              <h1 className='site-title'>Devseed Sense Lisbon
19
+                {/* <a href='/' title='Visit homepage'>Glacial Inferno</a> */}
20
+              </h1>
21
+            </div>
22
+          </div>
23
+        </header>
24
+
25
+        <main className='site-body' role='main'>
26
+          {this.props.children}
27
+        </main>
28
+      </div>
29
+    );
30
+  }
31
+});
32
+
33
+module.exports = App;

+ 263
- 0
app/assets/scripts/views/home.js View File

@@ -0,0 +1,263 @@
1
+'use strict';
2
+import React from 'react';
3
+import { connect } from 'react-redux';
4
+import _ from 'lodash';
5
+import SensorWidget from '../components/sensor-widget';
6
+import { fetchSensorData } from '../actions/action-creators';
7
+import { round } from '../utils/format';
8
+
9
+const getTime = function (str) {
10
+  let date = new Date(str);
11
+  return Math.floor(date.getTime() / 1000);
12
+};
13
+
14
+const sensorProps = React.PropTypes.shape({
15
+  fetched: React.PropTypes.bool,
16
+  fetching: React.PropTypes.bool,
17
+  data: React.PropTypes.array
18
+});
19
+
20
+var Home = React.createClass({
21
+  displayName: 'Home',
22
+
23
+  propTypes: {
24
+    _requestSensorData: React.PropTypes.func,
25
+    sensorUv: sensorProps,
26
+    sensorLuminosity: sensorProps,
27
+    sensorPressure: sensorProps,
28
+    sensorHumidity: sensorProps,
29
+    sensorTemperature: sensorProps
30
+  },
31
+
32
+  // Having measurements every minute is too much. Group them.
33
+  // Max seconds between reading for them to be considered part
34
+  // of the same group.
35
+  _mTimeThreshold: 120,
36
+  _mGroupSize: 6, // Equal to 10 measurements per hour.
37
+
38
+  _fetchInterval: null,
39
+  // In seconds.
40
+  _fetchRate: 300, // 5 min
41
+
42
+  prepareData: function (rawData) {
43
+    var data = null;
44
+
45
+    if (rawData) {
46
+      data = [];
47
+      rawData[0].value = +rawData[0].value;
48
+      let bucket = [rawData[0]];
49
+      for (var i = 1; i < rawData.length; i++) {
50
+        rawData[i].value = +rawData[i].value;
51
+        let prevTime = getTime(rawData[i - 1].createdAt);
52
+        let currTime = getTime(rawData[i].createdAt);
53
+        // Having measurements every minute is too much. Group them.
54
+        // To make sure that the grouped measurements are all around the same
55
+        // time there can't be more than X seconds difference between them.
56
+        if (currTime - prevTime > this._mTimeThreshold || bucket.length === this._mGroupSize) {
57
+          let f = {
58
+            createdAt: _.last(bucket).createdAt,
59
+            value: +round(_.meanBy(bucket, 'value'))
60
+          };
61
+          data.push(f);
62
+          bucket = [];
63
+        }
64
+        bucket.push(rawData[i]);
65
+      }
66
+      // After the loop finished there may still be data to process.
67
+      // if bucket.length < this._mGroupSize for example.
68
+      let f = {
69
+        createdAt: _.last(bucket).createdAt,
70
+        value: +round(_.meanBy(bucket, 'value'))
71
+      };
72
+      data.push(f);
73
+    }
74
+
75
+    let startToday = new Date();
76
+    startToday.setHours(0);
77
+    startToday.setMinutes(0);
78
+    startToday.setSeconds(0);
79
+
80
+    startToday = Math.floor(startToday.getTime() / 1000);
81
+    let startYesterday = startToday - (60 * 60 * 24);
82
+
83
+    let dataAll = [];
84
+    let dataToday = [];
85
+    let dataYesterday = [];
86
+
87
+    _.forEach(data, o => {
88
+      let date = new Date(o.createdAt);
89
+      let time = Math.floor(date.getTime() / 1000);
90
+      dataAll.push({
91
+        timestep: date,
92
+        value: +o.value
93
+      });
94
+      if (time >= startToday) {
95
+        dataToday.push({
96
+          timestep: date,
97
+          value: +o.value
98
+        });
99
+      }
100
+      if (time < startToday && time >= startYesterday) {
101
+        dataYesterday.push({
102
+          timestep: date,
103
+          value: +o.value
104
+        });
105
+      }
106
+    });
107
+
108
+    let avgs = {
109
+      today: _.meanBy(dataToday, 'value'),
110
+      yesterday: _.meanBy(dataYesterday, 'value')
111
+    };
112
+
113
+    let last = _.last(dataAll) || null;
114
+
115
+    return {
116
+      data: dataAll,
117
+      last,
118
+      avgs
119
+    };
120
+  },
121
+
122
+  fetchData: function () {
123
+    let daysAgo3 = (new Date()).getTime() - (60 * 60 * 24 * 3 * 1000);
124
+    daysAgo3 = new Date(daysAgo3).toISOString();
125
+    this.props._requestSensorData('temperature', daysAgo3);
126
+    this.props._requestSensorData('humidity', daysAgo3);
127
+    this.props._requestSensorData('uv', daysAgo3);
128
+    this.props._requestSensorData('luminosity', daysAgo3);
129
+    this.props._requestSensorData('pressure', daysAgo3);
130
+  },
131
+
132
+  componentDidMount: function () {
133
+    this.fetchData();
134
+    this._fetchInterval = setInterval(() => {
135
+      this.fetchData();
136
+    }, this._fetchRate * 1000);
137
+  },
138
+
139
+  componentWillUnmount: function () {
140
+    if (this._fetchInterval) {
141
+      clearInterval(this._fetchInterval);
142
+    }
143
+  },
144
+
145
+  render: function () {
146
+    let sensorTemperatureData = this.prepareData(this.props.sensorTemperature.data);
147
+    let sensorHumidityData = this.prepareData(this.props.sensorHumidity.data);
148
+    let sensorUvData = this.prepareData(this.props.sensorUv.data);
149
+    let sensorLuminosityData = this.prepareData(this.props.sensorLuminosity.data);
150
+    let sensorPressureData = this.prepareData(this.props.sensorPressure.data);
151
+
152
+    return (
153
+      <section className='page'>
154
+        <header className='page__header'>
155
+          <div className='inner'>
156
+            <div className='page__headline'>
157
+              <h1 className='page__title'>Sense Dashboard</h1>
158
+            </div>
159
+          </div>
160
+        </header>
161
+        <div className='page__body'>
162
+
163
+          <section className='page__content'>
164
+            <div className='inner'>
165
+
166
+              <SensorWidget
167
+                className='card--temp'
168
+                fetching={this.props.sensorTemperature.fetching}
169
+                fetched={this.props.sensorTemperature.fetched}
170
+                title='Temperature'
171
+                lastReading={sensorTemperatureData.last}
172
+                avgs={sensorTemperatureData.avgs}
173
+                plotData={sensorTemperatureData.data}
174
+                axisLineMax={35}
175
+                axisLineVal={20}
176
+                axisLineMin={4}
177
+                unit=' ºC'
178
+              />
179
+
180
+              <SensorWidget
181
+                className='card--hum'
182
+                fetching={this.props.sensorHumidity.fetching}
183
+                fetched={this.props.sensorHumidity.fetched}
184
+                title='Humidity'
185
+                lastReading={sensorHumidityData.last}
186
+                avgs={sensorHumidityData.avgs}
187
+                plotData={sensorHumidityData.data}
188
+                axisLineMax={100}
189
+                axisLineVal={50}
190
+                axisLineMin={10}
191
+                unit=' %'
192
+              />
193
+
194
+              <SensorWidget
195
+                className='card--uv'
196
+                fetching={this.props.sensorUv.fetching}
197
+                fetched={this.props.sensorUv.fetched}
198
+                title='Uv light'
199
+                lastReading={sensorUvData.last}
200
+                avgs={sensorUvData.avgs}
201
+                plotData={sensorUvData.data}
202
+                axisLineMax={5000}
203
+                axisLineVal={250}
204
+                axisLineMin={0}
205
+                unit=' μW/cm²'
206
+              />
207
+
208
+              <SensorWidget
209
+                className='card--lux'
210
+                fetching={this.props.sensorLuminosity.fetching}
211
+                fetched={this.props.sensorLuminosity.fetched}
212
+                title='Luminosity'
213
+                lastReading={sensorLuminosityData.last}
214
+                avgs={sensorLuminosityData.avgs}
215
+                plotData={sensorLuminosityData.data}
216
+                axisLineMax={135000}
217
+                axisLineVal={50000}
218
+                axisLineMin={0}
219
+                unit=' lx'
220
+              />
221
+
222
+              <SensorWidget
223
+                className='card--press'
224
+                fetching={this.props.sensorPressure.fetching}
225
+                fetched={this.props.sensorPressure.fetched}
226
+                title='Air Pressure'
227
+                lastReading={sensorPressureData.last}
228
+                avgs={sensorPressureData.avgs}
229
+                plotData={sensorPressureData.data}
230
+                axisLineMax={1020}
231
+                axisLineVal={1010}
232
+                axisLineMin={1000}
233
+                unit=' hPa'
234
+              />
235
+
236
+            </div>
237
+          </section>
238
+        </div>
239
+      </section>
240
+    );
241
+  }
242
+});
243
+
244
+// /////////////////////////////////////////////////////////////////// //
245
+// Connect functions
246
+
247
+function selector (state) {
248
+  return {
249
+    sensorUv: state.sensorUv,
250
+    sensorLuminosity: state.sensorLuminosity,
251
+    sensorPressure: state.sensorPressure,
252
+    sensorHumidity: state.sensorHumidity,
253
+    sensorTemperature: state.sensorTemperature
254
+  };
255
+}
256
+
257
+function dispatcher (dispatch) {
258
+  return {
259
+    _requestSensorData: (sensor, toDate) => dispatch(fetchSensorData(sensor, toDate))
260
+  };
261
+}
262
+
263
+module.exports = connect(selector, dispatcher)(Home);

+ 29
- 0
app/assets/scripts/views/uhoh.js View File

@@ -0,0 +1,29 @@
1
+'use strict';
2
+import React from 'react';
3
+
4
+var UhOh = React.createClass({
5
+  displayName: 'UhOh',
6
+
7
+  render: function () {
8
+    return (
9
+      <section className='page'>
10
+        <header className='page__header'>
11
+          <div className='inner'>
12
+            <div className='page__headline'>
13
+              <h1 className='page-title'>404 Not found</h1>
14
+            </div>
15
+          </div>
16
+        </header>
17
+        <div className='page__body'>
18
+          <div className='inner'>
19
+            <div className='page__content'>
20
+              <p>UhOh that is a bummer.</p>
21
+            </div>
22
+          </div>
23
+        </div>
24
+      </section>
25
+    );
26
+  }
27
+});
28
+
29
+module.exports = UhOh;

+ 282
- 0
app/assets/styles/_base.scss View File

@@ -0,0 +1,282 @@
1
+/* ==========================================================================
2
+   Base
3
+   ========================================================================== */
4
+
5
+html {
6
+  box-sizing: border-box;
7
+  font-size: 16px;
8
+}
9
+
10
+*, *:before, *:after, input[type="search"] {
11
+  box-sizing: inherit;
12
+}
13
+
14
+html, body {
15
+  height: 100%;
16
+}
17
+
18
+body {
19
+  background: #fff;
20
+  color: $base-font-color;
21
+  font-size: 1rem;
22
+  line-height: 1.5;
23
+  font-family: $base-font-family;
24
+  font-weight: $base-font-weight;
25
+  font-style: $base-font-style;
26
+  min-width: $row-min-width;
27
+}
28
+
29
+
30
+/* Links
31
+   ========================================================================== */
32
+
33
+a {
34
+  cursor: pointer;
35
+  color: $link-color;
36
+  text-decoration: none;
37
+  transition: opacity 0.24s ease 0s;
38
+}
39
+
40
+a:visited {
41
+  color: $link-color;
42
+}
43
+
44
+a:hover {
45
+  opacity: 0.64;
46
+  outline: none;
47
+}
48
+
49
+a:active {
50
+  outline: none;
51
+  transform: translate(0, 1px);
52
+}
53
+
54
+
55
+/* Rows
56
+   ========================================================================== */
57
+
58
+.row {
59
+  @extend .clearfix;
60
+  padding-left: $global-spacing;
61
+  padding-right: $global-spacing;
62
+  @include media(small-up) {
63
+    padding-left: $global-spacing * 2;
64
+    padding-right: $global-spacing * 2;
65
+  }
66
+  @include media(xlarge-up) {
67
+    padding-left: $global-spacing * 4;
68
+    padding-right: $global-spacing * 4;
69
+  }
70
+}
71
+
72
+.row--centered {
73
+  max-width: $row-max-width;
74
+  margin-left: auto;
75
+  margin-right: auto;
76
+}
77
+
78
+
79
+/* ==========================================================================
80
+   Structure
81
+   ========================================================================== */
82
+
83
+
84
+/* Header
85
+   ========================================================================== */
86
+
87
+.site-header {
88
+  position: absolute;
89
+  width: 100%;
90
+  z-index: 1000;
91
+  background-color: #fff;
92
+  color: $base-color;
93
+  padding: $global-spacing 0;
94
+  box-shadow: inset 0 -1px 0 0 rgba($base-color, 0.12);
95
+  > .inner {
96
+    // @extend .row, .row--centered;
97
+    padding: 0 2rem;
98
+  }
99
+  @include media(medium-up) {
100
+    padding: ($global-spacing * 2) 0;
101
+  }
102
+}
103
+
104
+/* Headline */
105
+.page__header {
106
+  padding-top: 5rem;
107
+  @include media(medium-up) {
108
+    padding-top: 8rem;
109
+  }
110
+}
111
+
112
+.page__title {
113
+  @extend .visually-hidden;
114
+}
115
+
116
+.site-headline {
117
+  @include col(12/12);
118
+  @include media(medium-up) {
119
+    @include col(6/12);
120
+  }
121
+}
122
+
123
+.site-title {
124
+  float: left;
125
+  margin: 0;
126
+  line-height: 1;
127
+  font-size: 1.25rem;
128
+  text-transform: uppercase;
129
+
130
+  @include media(medium-up) {
131
+    font-size: 1.75rem;
132
+  }
133
+
134
+  a {
135
+    display: block;
136
+  }
137
+  * {
138
+    vertical-align: top;
139
+    display: inline-block;
140
+  }
141
+  img {
142
+    width: auto;
143
+    height: 1rem;
144
+  }
145
+  span {
146
+    @extend .visually-hidden;
147
+  }
148
+}
149
+
150
+/* Body
151
+   ========================================================================== */
152
+
153
+.page__content {
154
+  > .inner {
155
+    // @extend .row, .row--centered;
156
+    padding: 0 2rem;
157
+  }
158
+}
159
+
160
+.card {
161
+  background: #fff;
162
+  box-shadow: 0 4px 0 0 rgba($base-color, 0.08), 0 0 0 1px $base-alpha-color;
163
+  border-radius: $global-radius;
164
+  position: relative;
165
+  overflow: hidden;
166
+  margin-bottom: 2rem;
167
+
168
+  @include media(large-up) {
169
+    @include col(6/12, $cycle: 2);
170
+  }
171
+  @include media(xlarge-up) {
172
+    @include col(4/12, $cycle: 3, $uncycle: 2);
173
+  }
174
+
175
+  &__header {
176
+    @extend .antialiased;
177
+    position: absolute;
178
+    padding: $global-spacing;
179
+    color: #fff;
180
+    top: 0;
181
+    right: 0;
182
+    left: 0;
183
+  }
184
+
185
+  &__title {
186
+    padding-right: 3rem;
187
+    font-size: 1.375rem;
188
+    margin-bottom: 0.5rem;
189
+    text-transform: uppercase;
190
+  }
191
+
192
+  &__loading {
193
+    padding: 3rem 0;
194
+    text-align: center;
195
+    color: #fff;
196
+    text-transform: uppercase;
197
+  }
198
+
199
+  .infographic {
200
+    padding: 5rem 1rem 1rem 1rem;
201
+  }
202
+
203
+  .stats {
204
+    &__label {
205
+      @extend .visually-hidden;
206
+    }
207
+
208
+    &__value {
209
+      position: absolute;
210
+      top: 1rem;
211
+      right: 1rem;
212
+      font-size: 1.375rem;
213
+      font-weight: $base-font-bold;
214
+    }
215
+
216
+    &__date {
217
+      font-size: 0.875rem;
218
+      line-height: 1rem;
219
+      color: rgba(#fff, 0.80);
220
+      &:before {
221
+        font-size: 1rem;
222
+        vertical-align: top;
223
+        margin-right: 0.5rem;
224
+        @extend %collecticons-clock;
225
+      }
226
+    }
227
+  }
228
+
229
+  .metrics {
230
+    padding: 1rem;
231
+
232
+    &__list  {
233
+      @extend .clearfix;
234
+      li {
235
+        @include col(6/12);
236
+        text-align: center;
237
+
238
+        &:not(:last-child) {
239
+          box-shadow: 1px 0 0 0 rgba($base-color, 0.16);
240
+        }
241
+      }
242
+
243
+      strong {
244
+        font-size: 1.625rem;
245
+        display: block;
246
+      }
247
+    }
248
+  }
249
+
250
+  &.card--temp {
251
+    .infographic {
252
+      background: linear-gradient(#FF5722, #E64A19);
253
+    }
254
+  }
255
+
256
+  &.card--hum {
257
+    .infographic {
258
+      background: linear-gradient(#009688, #0097A7);
259
+    }
260
+  }
261
+
262
+  &.card--uv {
263
+    .infographic {
264
+      background: linear-gradient(#7B1FA2, #9C27B0);
265
+    }
266
+  }
267
+
268
+  &.card--lux {
269
+    .infographic {
270
+      background: linear-gradient(#FFA000, #FFC107);
271
+    }
272
+  }
273
+
274
+  &.card--press {
275
+    .infographic {
276
+      background: linear-gradient(#616161, #607D8B);
277
+    }
278
+  }
279
+}
280
+
281
+/* Footer
282
+   ========================================================================== */

+ 45
- 0
app/assets/styles/_charts.scss View File

@@ -0,0 +1,45 @@
1
+/* Footer
2
+   ========================================================================== */
3
+svg.chart {
4
+  // Avoids that the chart grows because of the gap left by inline elements.
5
+  display: block;
6
+}
7
+
8
+.line-chart-wrapper {
9
+  min-height: 10rem;
10
+}
11
+
12
+.line-chart {
13
+  .data-line {
14
+    stroke: #fff;
15
+    stroke-width: 2px;
16
+    fill: none;
17
+  }
18
+
19
+  .edge {
20
+    font-size: 0.75rem;
21
+    fill: rgba(#fff, 0.8);
22
+  }
23
+
24
+  .days {
25
+    font-size: 0.75rem;
26
+    fill: rgba(#fff, 0.8);
27
+  }
28
+
29
+  .x.axis text {
30
+    font-size: 0.75rem;
31
+    fill: rgba(#fff, 0.8);
32
+  }
33
+
34
+  .y.axis {
35
+    .line {
36
+      stroke: rgba(#fff, 0.8);
37
+      stroke-dasharray: 4 4;  
38
+    }
39
+    .label {
40
+      @extend .antialiased;
41
+      fill: rgba(#fff, 0.8);
42
+      font-size: 0.75rem;
43
+    }
44
+  }
45
+}

+ 24
- 0
app/assets/styles/_functions.scss View File

@@ -0,0 +1,24 @@
1
+/* ==========================================================================
2
+   Functions
3
+   ========================================================================== */
4
+
5
+/* Range function
6
+   ========================================================================== */
7
+
8
+/**
9
+ * Define ranges for various things, like media queries. 
10
+ */
11
+
12
+@function lower-bound($range){
13
+  @if length($range) <= 0 {
14
+    @return 0;
15
+  }
16
+  @return nth($range,1);
17
+}
18
+
19
+@function upper-bound($range) {
20
+  @if length($range) < 2 {
21
+    @return 999999999999;
22
+  }
23
+  @return nth($range, 2);
24
+}

+ 143
- 0
app/assets/styles/_mixins.scss View File

@@ -0,0 +1,143 @@
1
+/* ==========================================================================
2
+   Media queries
3
+   ========================================================================== */
4
+
5
+@mixin media($arg) {
6
+  @if $arg == screen {
7
+    @media #{$screen} { @content; }
8
+  }
9
+  @if $arg == landscape {
10
+    @media #{$screen} and (orientation: landscape) { @content; }
11
+  }
12
+  @if $arg == portrait {
13
+    @media #{$screen} and (orientation: portrait) { @content; }
14
+  }
15
+  @if $arg == xsmall-up {
16
+    @media #{$screen} and (min-width: lower-bound($xsmall-range)) { @content; }
17
+  }
18
+  @if $arg == xsmall-only {
19
+    @media #{$screen} and (max-width: upper-bound($xsmall-range)) { @content; }
20
+  }
21
+  @if $arg == small-up {
22
+    @media #{$screen} and (min-width: lower-bound($small-range)) { @content; }
23
+  }
24
+  @if $arg == small-only {
25
+    @media #{$screen} and (min-width: lower-bound($small-range)) and (max-width: upper-bound($small-range)) { @content; }
26
+  }
27
+  @if $arg == medium-up {
28
+    @media #{$screen} and (min-width: lower-bound($medium-range)) { @content; }
29
+  }
30
+  @if $arg == medium-down {
31
+    @media #{$screen} and (max-width: upper-bound($medium-range)) { @content; }
32
+  }
33
+  @if $arg == medium-only {
34
+    @media #{$screen} and (min-width: lower-bound($medium-range)) and (max-width: upper-bound($medium-range)) { @content; }
35
+  }
36
+  @if $arg == large-up {
37
+    @media #{$screen} and (min-width: lower-bound($large-range)) { @content; }
38
+  }
39
+  @if $arg == large-only {
40
+    @media #{$screen} and (min-width: lower-bound($large-range)) and (max-width: upper-bound($large-range)) { @content; }
41
+  }
42
+  @if $arg == xlarge-up {
43
+    @media #{$screen} and (min-width: lower-bound($xlarge-range)) { @content; }
44
+  }
45
+  @if $arg == xlarge-only {
46
+    @media #{$screen} and (min-width: lower-bound($xlarge-range)) and (max-width: upper-bound($xlarge-range)) { @content; }
47
+  }
48
+  @if $arg == xxlarge-up {
49
+    @media #{$screen} and (min-width: lower-bound($xxlarge-range)) { @content; }
50
+  }
51
+}
52
+
53
+
54
+/* ==========================================================================
55
+   Typography
56
+   ========================================================================== */
57
+
58
+@mixin heading($font-size, $max-media: small-up) {
59
+  font-size: $font-size;
60
+  line-height: $font-size + 0.5;
61
+
62
+  @if $max-media == medium-up or $max-media == large-up or $max-media == xlarge-up {
63
+    @include media(medium-up) {
64
+      $font-size: $font-size + 0.25;
65
+      font-size: $font-size;
66
+      line-height: $font-size + 0.5;
67
+    }
68
+  }
69
+
70
+  @if $max-media == large-up or $max-media == xlarge-up {
71
+    @include media(large-up) {
72
+      $font-size: $font-size + 0.25;
73
+      font-size: $font-size;
74
+      line-height: $font-size +  0.5;
75
+    }
76
+  }
77
+
78
+  @if $max-media == xlarge-up {
79
+    @include media(xlarge-up) {
80
+      $font-size: $font-size + 0.25;
81
+      font-size: $font-size;
82
+      line-height: $font-size + 0.5;
83
+    }
84
+  }
85
+}
86
+
87
+
88
+/* ==========================================================================
89
+   Buttons
90
+   ========================================================================== */
91
+
92
+@mixin button-variation($style, $color) {
93
+  @if $style == "filled" {
94
+    background-color: $color;
95
+    box-shadow: inset 0 0 0 1px rgba($base-color, 0.16);
96
+    &, &:visited {
97
+      color: #fff;
98
+    }
99
+    &:hover {
100
+      background-color: shade($color, 8%);
101
+    }
102
+    .drop--open > &,
103
+    &.button--active,
104
+    &.button--active:hover,
105
+    &:active {
106
+      background-color: shade($color, 16%);
107
+    }
108
+  }
109
+  @else if $style == "outline" {
110
+    background-color: rgba($color, 0);
111
+    box-shadow: inset 0 0 0 1px $color;
112
+    &, &:visited {
113
+      color: $color;
114
+    }
115
+    &:hover {
116
+      background-color: rgba($color, 0.08);
117
+    }
118
+    .open > &,
119
+    &.active,
120
+    &.active:hover,
121
+    &:active {
122
+      background-color: rgba($color, 0.16);
123
+    }
124
+  }
125
+  @else if $style == "unbounded" {
126
+    background-color: rgba($color, 0);
127
+    &, &:visited {
128
+      color: $color;
129
+    }
130
+    &:hover {
131
+      background-color: rgba($color, 0.08);
132
+    }
133
+    .open > &,
134
+    &.active,
135
+    &.active:hover,
136
+    &:active {
137
+      color: shade($color, 48%);
138
+    }
139
+  }
140
+  @else {
141
+    @error "Invalid style property for button variation.";
142
+  }
143
+}

+ 425
- 0
app/assets/styles/_normalize.scss View File

@@ -0,0 +1,425 @@
1
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
2
+
3
+/**
4
+ * 1. Set default font family to sans-serif.
5
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
6
+ *    user zoom.
7
+ */
8
+
9
+html {
10
+  font-family: sans-serif; /* 1 */
11
+  -ms-text-size-adjust: 100%; /* 2 */
12
+  -webkit-text-size-adjust: 100%; /* 2 */
13
+}
14
+
15
+/**
16
+ * Remove default margin.
17
+ */
18
+
19
+body {
20
+  margin: 0;
21
+}
22
+
23
+/* HTML5 display definitions
24
+   ========================================================================== */
25
+
26
+/**
27
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
28
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
29
+ * Correct `block` display not defined for `main` in IE 11.
30
+ */
31
+
32
+article,
33
+aside,
34
+details,
35
+figcaption,
36
+figure,
37
+footer,
38
+header,
39
+hgroup,
40
+main,
41
+nav,
42
+section,
43
+summary {
44
+  display: block;
45
+}
46
+
47
+/**
48
+ * 1. Correct `inline-block` display not defined in IE 8/9.
49
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
50
+ */
51
+
52
+audio,
53
+canvas,
54
+progress,
55
+video {
56
+  display: inline-block; /* 1 */
57
+  vertical-align: baseline; /* 2 */
58
+}
59
+
60
+/**
61
+ * Prevent modern browsers from displaying `audio` without controls.
62
+ * Remove excess height in iOS 5 devices.
63
+ */
64
+
65
+audio:not([controls]) {
66
+  display: none;
67
+  height: 0;
68
+}
69
+
70
+/**
71
+ * Address `[hidden]` styling not present in IE 8/9/10.
72
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
73
+ */
74
+
75
+[hidden],
76
+template {
77
+  display: none;
78
+}
79
+
80
+/* Links
81
+   ========================================================================== */
82
+
83
+/**
84
+ * Remove the gray background color from active links in IE 10.
85
+ */
86
+
87
+a {
88
+  background: transparent;
89
+}
90
+
91
+/**
92
+ * Improve readability when focused and also mouse hovered in all browsers.
93
+ */
94
+
95
+a:active,
96
+a:hover {
97
+  outline: 0;
98
+}
99
+
100
+/* Text-level semantics
101
+   ========================================================================== */
102
+
103
+/**
104
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
105
+ */
106
+
107
+abbr[title] {
108
+  border-bottom: 1px dotted;
109
+}
110
+
111
+/**
112
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
113
+ */
114
+
115
+b,
116
+strong {
117
+  font-weight: bold;
118
+}
119
+
120
+/**
121
+ * Address styling not present in Safari and Chrome.
122
+ */
123
+
124
+dfn {
125
+  font-style: italic;
126
+}
127
+
128
+/**
129
+ * Address variable `h1` font-size and margin within `section` and `article`
130
+ * contexts in Firefox 4+, Safari, and Chrome.
131
+ */
132
+
133
+// h1 {
134
+//   font-size: 2em;
135
+//   margin: 0.67em 0;
136
+// }
137
+
138
+/**
139
+ * Address styling not present in IE 8/9.
140
+ */
141
+
142
+mark {
143
+  background: #ff0;
144
+  color: #000;
145
+}
146
+
147
+/**
148
+ * Address inconsistent and variable font size in all browsers.
149
+ */
150
+
151
+small {
152
+  font-size: 80%;
153
+}
154
+
155
+/**
156
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
157
+ */
158
+
159
+sub,
160
+sup {
161
+  font-size: 75%;
162
+  line-height: 0;
163
+  position: relative;
164
+  vertical-align: baseline;
165
+}
166
+
167
+sup {
168
+  top: -0.5em;
169
+}
170
+
171
+sub {
172
+  bottom: -0.25em;
173
+}
174
+
175
+/* Embedded content
176
+   ========================================================================== */
177
+
178
+/**
179
+ * Remove border when inside `a` element in IE 8/9/10.
180
+ */
181
+
182
+img {
183
+  border: 0;
184
+}
185
+
186
+/**
187
+ * Correct overflow not hidden in IE 9/10/11.
188
+ */
189
+
190
+svg:not(:root) {
191
+  overflow: hidden;
192
+}
193
+
194
+/* Grouping content
195
+   ========================================================================== */
196
+
197
+/**
198
+ * Address margin not present in IE 8/9 and Safari.
199
+ */
200
+
201
+figure {
202
+  margin: 1em 40px;
203
+}
204
+
205
+/**
206
+ * Address differences between Firefox and other browsers.
207
+ */
208
+
209
+hr {
210
+  -moz-box-sizing: content-box;
211
+  box-sizing: content-box;
212
+  height: 0;
213
+}
214
+
215
+/**
216
+ * Contain overflow in all browsers.
217
+ */
218
+
219
+pre {
220
+  overflow: auto;
221
+}
222
+
223
+/**
224
+ * Address odd `em`-unit font size rendering in all browsers.
225
+ */
226
+
227
+code,
228
+kbd,
229
+pre,
230
+samp {
231
+  font-family: monospace, monospace;
232
+  font-size: 1em;
233
+}
234
+
235
+/* Forms
236
+   ========================================================================== */
237
+
238
+/**
239
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
240
+ * styling of `select`, unless a `border` property is set.
241
+ */
242
+
243
+/**
244
+ * 1. Correct color not being inherited.
245
+ *    Known issue: affects color of disabled elements.
246
+ * 2. Correct font properties not being inherited.
247
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
248
+ */
249
+
250
+button,
251
+input,
252
+optgroup,
253
+select,
254
+textarea {
255
+  color: inherit; /* 1 */
256
+  font: inherit; /* 2 */
257
+  margin: 0; /* 3 */
258
+}
259
+
260
+/**
261
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
262
+ */
263
+
264
+button {
265
+  overflow: visible;
266
+}
267
+
268
+/**
269
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
270
+ * All other form control elements do not inherit `text-transform` values.
271
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
272
+ * Correct `select` style inheritance in Firefox.
273
+ */
274
+
275
+button,
276
+select {
277
+  text-transform: none;
278
+}
279
+
280
+/**
281
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
282
+ *    and `video` controls.
283
+ * 2. Correct inability to style clickable `input` types in iOS.
284
+ * 3. Improve usability and consistency of cursor style between image-type
285
+ *    `input` and others.
286
+ */
287
+
288
+button,
289
+html input[type="button"], /* 1 */
290
+input[type="reset"],
291
+input[type="submit"] {
292
+  -webkit-appearance: button; /* 2 */
293
+  cursor: pointer; /* 3 */
294
+}
295
+
296
+/**
297
+ * Re-set default cursor for disabled elements.
298
+ */
299
+
300
+button[disabled],
301
+html input[disabled] {
302
+  cursor: default;
303
+}
304
+
305
+/**
306
+ * Remove inner padding and border in Firefox 4+.
307
+ */
308
+
309
+button::-moz-focus-inner,
310
+input::-moz-focus-inner {
311
+  border: 0;
312
+  padding: 0;
313
+}
314
+
315
+/**
316
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
317
+ * the UA stylesheet.
318
+ */
319
+
320
+input {
321
+  line-height: normal;
322
+}
323
+
324
+/**
325
+ * It's recommended that you don't attempt to style these elements.
326
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
327
+ *
328
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
329
+ * 2. Remove excess padding in IE 8/9/10.
330
+ */
331
+
332
+input[type="checkbox"],
333
+input[type="radio"] {
334
+  box-sizing: border-box; /* 1 */
335
+  padding: 0; /* 2 */
336
+}
337
+
338
+/**
339
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
340
+ * `font-size` values of the `input`, it causes the cursor style of the
341
+ * decrement button to change from `default` to `text`.
342
+ */
343
+
344
+input[type="number"]::-webkit-inner-spin-button,
345
+input[type="number"]::-webkit-outer-spin-button {
346
+  height: auto;
347
+}
348
+
349
+/**
350
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
351
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
352
+ *    (include `-moz` to future-proof).
353
+ */
354
+
355
+input[type="search"] {
356
+  -webkit-appearance: textfield; /* 1 */
357
+  -moz-box-sizing: content-box;
358
+  -webkit-box-sizing: content-box; /* 2 */
359
+  box-sizing: content-box;
360
+}
361
+
362
+/**
363
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
364
+ * Safari (but not Chrome) clips the cancel button when the search input has
365
+ * padding (and `textfield` appearance).
366
+ */
367
+
368
+input[type="search"]::-webkit-search-cancel-button,
369
+input[type="search"]::-webkit-search-decoration {
370
+  -webkit-appearance: none;
371
+}
372
+
373
+/**
374
+ * Define consistent border, margin, and padding.
375
+ */
376
+
377
+fieldset {
378
+  border: 1px solid #c0c0c0;
379
+  margin: 0 2px;
380
+  padding: 0.35em 0.625em 0.75em;
381
+}
382
+
383
+/**
384
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
385
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
386
+ */
387
+
388
+legend {
389
+  border: 0; /* 1 */
390
+  padding: 0; /* 2 */
391
+}
392
+
393
+/**
394
+ * Remove default vertical scrollbar in IE 8/9/10/11.
395
+ */
396
+
397
+textarea {
398
+  overflow: auto;
399
+}
400
+
401
+/**
402
+ * Don't inherit the `font-weight` (applied by a rule above).
403
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
404
+ */
405
+
406
+optgroup {
407
+  font-weight: bold;
408
+}
409
+
410
+/* Tables
411
+   ========================================================================== */
412
+
413
+/**
414
+ * Remove most spacing between table cells.
415
+ */
416
+
417
+table {
418
+  border-collapse: collapse;
419
+  border-spacing: 0;
420
+}
421
+
422
+td,
423
+th {
424
+  padding: 0;
425
+}

+ 143
- 0
app/assets/styles/_reset.scss View File

@@ -0,0 +1,143 @@
1
+/**
2
+ * Reset ==============================================================
3
+ * Based on http://meyerweb.com/eric/tools/css/reset
4
+ */
5
+
6
+html,
7
+body,
8
+
9
+/* Structures */
10
+div,
11
+span,
12
+applet,
13
+object,
14
+iframe,
15
+
16
+/* Text */
17
+h1,
18
+h2,
19
+h3,
20
+h4,
21
+h5,
22
+h6,
23
+p,
24
+blockquote,
25
+pre,
26
+a,
27
+abbr,
28
+acronym,
29
+address,
30
+big,
31
+cite,
32
+code,
33
+del,
34
+dfn,
35
+em,
36
+font,
37
+img,
38
+ins,
39
+kbd,
40
+q,
41
+s,
42
+samp,
43
+small,
44
+strike,
45
+strong,
46
+sub,
47
+sup,
48
+tt,
49
+var,
50
+b,
51
+u,
52
+i,
53
+center,
54
+
55
+/* Lists */
56
+dl,
57
+dt,
58
+dd,
59
+ol,
60
+ul,
61
+li,
62
+
63
+/* Forms */
64
+fieldset,
65
+form,
66
+input,
67
+select,
68
+textarea,
69
+label,
70
+legend,
71
+
72
+/* Tables */
73
+table,
74
+caption,
75
+tbody,
76
+tfoot,
77
+thead,
78
+tr,
79
+th,
80
+td {
81
+  margin: 0;
82
+  padding: 0;
83
+  border: 0;
84
+  outline: 0;
85
+  font-size: 100%;
86
+  vertical-align: baseline;
87
+  background: transparent;
88
+  line-height: inherit;
89
+}
90
+
91
+ol,
92
+ul,
93
+.item-list ul,
94
+.item-list ul li {
95
+  list-style: none;
96
+}
97
+
98
+blockquote,
99
+q { quotes: none; }
100
+
101
+blockquote:before,
102
+blockquote:after,
103
+q:before, q:after {
104
+  content:'';
105
+  content: none;
106
+}
107
+
108
+/* remember to define focus styles! */
109
+:focus { outline: 0; }
110
+
111
+/* remember to highlight inserts somehow! */
112
+ins { text-decoration: none; }
113
+del { text-decoration: line-through; }
114
+
115
+button,
116
+html input[type=button],
117
+input[type=reset],
118
+input[type=submit] {
119
+  -webkit-appearance: button;
120
+  outline: none;
121
+  cursor: pointer;
122
+  font: inherit;
123
+  height: auto;
124
+  width: auto;
125
+  margin: 0;
126
+}
127
+
128
+input, select, textarea {
129
+  font: inherit;
130
+  height: auto;
131
+  width: auto;
132
+  margin: 0;
133
+}
134
+
135
+input[type="search"] {
136
+  -webkit-appearance: none;
137
+}
138
+
139
+legend,
140
+label {
141
+  font: inherit;
142
+  color: inherit;
143
+}

+ 105
- 0
app/assets/styles/_utils.scss View File

@@ -0,0 +1,105 @@
1
+/* ==========================================================================
2
+   Utils
3
+   ========================================================================== */
4
+
5
+/* Font smoothing
6
+   ========================================================================== */
7
+
8
+/**
9
+ * Antialiased font smoothing works best for light text on a dark background.
10
+ * Apply to single elements instead of globally to body.
11
+ * Note this only applies to webkit-based desktop browsers and Firefox 25 (and later) on the Mac.
12
+ */
13
+
14
+.antialiased {
15
+  -webkit-font-smoothing: antialiased;
16
+  -moz-osx-font-smoothing: grayscale;
17
+}
18
+
19
+/* Truncated text
20
+   ========================================================================== */
21
+
22
+.truncated {
23
+  white-space: nowrap;
24
+  overflow: hidden;
25
+  text-overflow: ellipsis;
26
+}
27
+
28
+/* Hidden content
29
+   ========================================================================== */
30
+
31
+/* Hide from both screenreaders and browsers */
32
+
33
+.hidden {
34
+  display: none !important;
35
+  visibility: hidden;
36
+}
37
+
38
+/* Hide only visually, but have it available for screenreaders */
39
+
40
+.visually-hidden {
41
+  border: 0 none;
42
+  clip: rect(0px, 0px, 0px, 0px);
43
+  height: 1px;
44
+  margin: -1px;
45
+  overflow: hidden;
46
+  padding: 0;
47
+  position: absolute;
48
+  width: 1px;
49
+}
50
+
51
+/**
52
+ * Extends the .visually-hidden class to allow the element
53
+ * to be focusable when navigated to via the keyboard
54
+ */
55
+
56
+.visually-hidden.focusable:active,
57
+.visually-hidden.focusable:focus {
58
+  clip: auto;
59
+  height: auto;
60
+  margin: 0;
61
+  overflow: visible;
62
+  position: static;
63
+  width: auto;
64
+}
65
+
66
+/* Undo visually-hidden */
67
+
68
+.visually-hidden-undo {
69
+  position: inherit;
70
+  overflow: visible;
71
+  height: auto;
72
+  width: auto;
73
+  margin: auto;
74
+}
75
+
76
+/* Hide visually and from screenreaders, but maintain layout */
77
+
78
+.invisible {
79
+  visibility: hidden;
80
+}
81
+
82
+
83
+/* Clearfix
84
+   ========================================================================== */
85
+
86
+.clearfix {
87
+  &:before,
88
+  &:after {
89
+    content: " ";
90
+    display: table;
91
+  }
92
+  &:after {
93
+    clear: both;
94
+  }
95
+}
96
+
97
+
98
+/* Disabled
99
+   ========================================================================== */
100
+
101
+.disabled {
102
+  opacity: 0.48;
103
+  pointer-events: none;
104
+  cursor: not-allowed;
105
+}

+ 70
- 0
app/assets/styles/_variables.scss View File

@@ -0,0 +1,70 @@
1
+/* ==========================================================================
2
+   Colors
3
+   ========================================================================== */
4
+$base-color: #2c3e50;                   // Midnight blue
5
+$primary-color: #16a085;                // Belize Hole
6
+$secondary-color: #3498db;              // Purple
7
+// $tertiary-color: #ffffff;            // Undefined
8
+
9
+$danger-color: #ea4f54;                 // Red
10
+$success-color: $primary-color;         // lime
11
+$warning-color: #f6d55f;                // Yellow
12
+$info-color: #008CBA;                   // Blue
13
+
14
+$link-color: $primary-color;
15
+$base-alpha-color: rgba($base-color, 0.12);
16
+
17
+
18
+/* ==========================================================================
19
+   Typography
20
+   ========================================================================== */
21
+
22
+/**
23
+ * $base-* refers to the base styles when there's nothing else to override them.
24
+ */
25
+$base-font-color: $base-color;
26
+$base-font-family: "Exo 2", "Helvetica Neue", Helvetica, Arial, sans-serif;
27
+$base-font-style: normal;
28
+$base-font-light: 300;
29
+$base-font-regular: 400;
30
+$base-font-bold: 700;
31
+$base-font-weight: $base-font-regular;
32
+
33
+$code-font-family: Menlo, Monaco, Consolas, "Courier New"