├── .babelrc
├── .eslintrc
├── .gitignore
├── LICENSE.txt
├── README.md
├── circle.yml
├── client
├── Realtime.js
├── UserId.js
└── app.js
├── config
├── default.json
└── production.json
├── dbSetup.babel.js
├── dbSetup.js
├── images
├── favicon.gif
└── favicon.ico
├── karma.conf.js
├── package.json
├── server.babel.js
├── server.js
├── server.webpack.js
├── server
├── api
│ ├── http.js
│ └── service
│ │ └── event.js
├── app.js
└── views
│ └── index.ejs
├── start-dev.js
├── style
├── main.styl
├── pure.css
└── spinner.styl
├── test
├── actions.test.js
└── reducers.test.js
├── tests.webpack.js
├── universal
├── actions
│ └── PulseActions.js
├── components
│ ├── AsyncBar.js
│ ├── EventInput.js
│ ├── EventItem.js
│ ├── EventList.js
│ ├── EventTicker.js
│ └── Header.js
├── constants
│ └── ActionTypes.js
├── containers
│ ├── MyEvents.js
│ ├── OtherEvents.js
│ ├── PulseApp.js
│ ├── devTools.js
│ └── root
│ │ ├── index.js
│ │ ├── root.dev.js
│ │ └── root.prod.js
├── reducers
│ ├── index.js
│ └── pulse.js
├── routes.js
└── store
│ ├── configureStore.client.dev.js
│ ├── configureStore.client.prod.js
│ ├── configureStore.server.js
│ └── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "comments": false,
3 | "presets": [
4 | "es2015",
5 | "stage-0",
6 | "react"
7 | ],
8 | "plugins": [
9 | "rewire",
10 | "system-import-transformer"
11 | ],
12 | "env": {
13 | "start": {
14 | "presets": ["react-hmre"]
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | // I want to use babel-eslint for parsing!
3 | "parser": "babel-eslint",
4 | "ecmaFeatures": {
5 | "jsx": true,
6 | "classes": true,
7 | "modules": true,
8 | },
9 | "env": {
10 | // I write for browser
11 | "browser": true,
12 | // in CommonJS
13 | "node": true
14 | },
15 | "globals": {
16 | "process": false,
17 | "require": false,
18 | "define": false,
19 | "console": false,
20 | },
21 | // To give you an idea how to override rule options:
22 | "rules": {
23 | "quotes": [2, "single"],
24 | "semi": [2, "always"],
25 | "strict": [2, "never"],
26 | "eol-last": [0],
27 | "no-mixed-requires": [0],
28 | "no-underscore-dangle": [0],
29 | "no-bitwise": 2,
30 | "camelcase": 2,
31 | "eqeqeq": 2,
32 | "wrap-iife": [2, "inside"],
33 | "no-use-before-define": [2, "nofunc"],
34 | "no-caller": 2,
35 | "no-undef": 2,
36 | "new-cap": 2,
37 | "react/jsx-uses-react": 2,
38 | "react/jsx-uses-vars": 2,
39 | "react/react-in-jsx-scope": 2
40 | },
41 | "plugins": [
42 | "react"
43 | ]
44 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | build
4 | config/local.json
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-present Gordon Dent
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 3REE
2 | [](https://circleci.com/gh/GordyD/3ree)
3 |
4 | An example universal JS application written with the 3REE stack, *Re*act + *Re*dux + *Re*thinkDB + *E*xpress. A stack for building apps, front and back end, with just Javascript.
5 |
6 | This project was initially conceived to experiment with using these technologies in conjunction with one-another. I have written a [blog](http://blog.workshape.io/the-3ree-stack-react-redux-rethinkdb-express-js/) that relates to this codebase.
7 |
8 | 
9 |
10 | This project is useful for:
11 | - seeing how to build a Universal Javascript application
12 | - understanding how to handle asyncronousity in Redux action creators
13 | - seeing how you can use Socket.io with Redux
14 | - building your own Redux powered application
15 | - seeing how you can use System.import() with React Router + Webpack2 to acheive code splitting for different routes of your application
16 | - forking so that you can build your own 3REE stack app!
17 |
18 | ### Main Features
19 |
20 | - Universal (Isomorphic) Javascript Application
21 | - Use of Webpack 2's Code Splitting and Tree Shaking features
22 | - Asyncronous Redux actions example
23 | - Use of RethinkDB Changefeeds for realtime updates reflected in the UI
24 |
25 | ### Demo
26 |
27 | There is a demo app hosted at [3ree-demo.workshape.io](http://3ree-demo.workshape.io). Check it out. If it is down, please email tanc@workshape.io
28 |
29 | ### Setup
30 |
31 | You will need to install [RethinkDB](http://www.rethinkdb.com). You can find instruction on how to do so [here](http://rethinkdb.com/docs/install/). Make sure you have the latest version installed.
32 |
33 | - Clone the repo `git clone git@github.com:GordyD/3ree.git`
34 | - Make sure you are using Node v6.0.0 (I recommend using [n](https://github.com/tj/n) for Node version management)
35 | - Run `npm install`
36 | - If your local environment is not reflected by `config/default.json`, then add a file at `config/local.json` to provide local customisation.
37 | - Run `npm run db-setup` to set up DB
38 |
39 | ### Running Dev Server
40 |
41 | On Linux/OSX: `npm start`
42 |
43 | On Windows: `npm run start:win`
44 |
45 | This will start the Webpack dev server - for serving the client, as well as the server-side API.
46 |
47 | Go to http://localhost:3001 in two separate tabs - see changes propagate in real time (Hot Module Replacement works too).
48 |
49 | ### Running Production Server
50 |
51 | You will need to roll out your own deployment script for a server, but before you can ship you will need to:
52 |
53 | - Build the client with `npm run build:prod`
54 | - Ensure all production npm modules are installed on the server. e.g. `npm install --prod`
55 | - Rsync your application to your server
56 | - Set up nginx or your web server of choice to map HTTP requests for your URL to `http://localhost:3000`
57 | - Run `npm run start:prod` to run on your server
58 | - Go to your URL
59 |
60 | NOTE: Production has not been tested on Windows.
61 |
62 | ### Tech Used
63 |
64 | | **Tech** | **Description** |
65 | | ---------|-----------------|
66 | | [React](https://facebook.github.io/react/) | View layer |
67 | | [React Router](https://github.com/reactjs/react-router) | Universal routing |
68 | | [Redux](http://redux.js.org/) | State management |
69 | | [RethinkDB](http://www.rethinkdb.com) | Persistance layer |
70 | | [Express](http://expressjs.com/) | Node.js server framework |
71 | | [Socket.io]() | Used for realtime communication between clients and server |
72 | | [Webpack](https://webpack.github.io/) | Module bundling + build for client |
73 | | [Superagent](https://github.com/visionmedia/superagent) | Universal http requests |
74 | | [Stylus](http://stylus-lang.com/) | Expressive, dynamic, robust CSS |
75 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 6.0.0
4 | test:
5 | override:
6 | - npm run lint
7 | - npm test
--------------------------------------------------------------------------------
/client/Realtime.js:
--------------------------------------------------------------------------------
1 | import socketClient from 'socket.io-client';
2 |
3 | export function setupRealtime(store, actions) {
4 | const io = socketClient();
5 |
6 | io.on('event-change', (change) => {
7 | let state = store.getState();
8 | if (!change.old_val) {
9 | store.dispatch(actions.addEventSuccess(change.new_val));
10 | } else if (!change.new_val) {
11 | store.dispatch(actions.deleteEventSuccess(change.old_val));
12 | } else {
13 | store.dispatch(actions.editEventSuccess(change.new_val));
14 | }
15 | });
16 |
17 | return io;
18 | }
--------------------------------------------------------------------------------
/client/UserId.js:
--------------------------------------------------------------------------------
1 | // This is a tiny UUID generator to replace node-uuid!
2 | // See: https://gist.github.com/jed/982883
3 |
4 | function uuid() {
5 | function b(a){
6 | /*eslint-disable */
7 | return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)
8 | /*eslint-enable */
9 | }
10 |
11 | return b();
12 | }
13 |
14 |
15 | export function hasLocalStorage() {
16 | return (!!window.localStorage);
17 | }
18 |
19 | export function getUserId() {
20 | return window.localStorage.getItem('userId');
21 | }
22 |
23 | export function setUserId() {
24 | let id = uuid();
25 | window.localStorage.setItem('userId', id);
26 | return id;
27 | }
28 |
29 | export function getOrSetUserId() {
30 | if (!hasLocalStorage()) {
31 | return 'baseUser';
32 | } else {
33 | let userId = getUserId();
34 | return (userId) ? userId : setUserId();
35 | }
36 | }
--------------------------------------------------------------------------------
/client/app.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import React from 'react';
3 | import { Router, Route, browserHistory } from 'react-router';
4 | import { syncHistoryWithStore } from 'react-router-redux';
5 |
6 | import { getOrSetUserId } from './UserId';
7 | import { setupRealtime } from './Realtime';
8 |
9 | import routes from '../universal/routes';
10 | import store from '../universal/store';
11 | import * as actions from '../universal/actions/PulseActions';
12 |
13 | import Root from '../universal/containers/root';
14 |
15 | import '../style/pure.css';
16 | import '../style/main.styl';
17 | import '../style/spinner.styl';
18 |
19 | const history = syncHistoryWithStore(browserHistory, store);
20 |
21 | ReactDOM.render(
22 | ,
23 | document.getElementById('app')
24 | );
25 |
26 | // Now that we have rendered...
27 | setupRealtime(store, actions);
28 |
29 | // lets mutate state and set UserID as key from local storage
30 | store.dispatch(actions.setUserId(getOrSetUserId()));
31 |
--------------------------------------------------------------------------------
/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "rethinkdb": {
3 | "host": "localhost",
4 | "port": 28015,
5 | "db": "pulse"
6 | },
7 | "express": {
8 | "host": "localhost",
9 | "port": 3000
10 | },
11 | "buildDirectory": "build"
12 | }
13 |
--------------------------------------------------------------------------------
/config/production.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildDirectory": "dist",
3 | "express": {
4 | "port": 3210
5 | }
6 | }
--------------------------------------------------------------------------------
/dbSetup.babel.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('./dbSetup');
3 |
--------------------------------------------------------------------------------
/dbSetup.js:
--------------------------------------------------------------------------------
1 | /* global Promise */
2 | import r from 'rethinkdb';
3 | import config from 'config';
4 |
5 | const rethinkdb = config.get('rethinkdb');
6 | let DATABASE = rethinkdb.db || 'pulse';
7 | let TABLES = ['pulses'];
8 |
9 | r.connect(rethinkdb)
10 | .then(conn => {
11 | console.log(' [-] Database Setup');
12 | return createDbIfNotExists(conn)
13 | .then(() => Promise.all(TABLES.map((table) => createTableIfNotExists(conn, table))))
14 | .then(() => closeConnection(conn));
15 | });
16 |
17 | function createDbIfNotExists(conn){
18 | return getDbList(conn)
19 | .then((list) => {
20 | if(list.indexOf(DATABASE) === -1) {
21 | return createDatabase(conn);
22 | } else {
23 | console.log(' [!] Database already exists:', DATABASE);
24 | return Promise.resolve(true);
25 | }
26 | });
27 | }
28 |
29 | function createTableIfNotExists(conn, table) {
30 | return getTableList(conn)
31 | .then((list) => {
32 | if(list.indexOf(table) === -1) {
33 | return createTable(conn, table);
34 | } else {
35 | console.log(' [!] Table already exists:', table);
36 | return Promise.resolve(true);
37 | }
38 | });
39 | }
40 |
41 | function getDbList(conn) {
42 | return r.dbList().run(conn);
43 | }
44 |
45 | function getTableList(conn) {
46 | return r.db(DATABASE).tableList().run(conn);
47 | }
48 |
49 | function createDatabase(conn) {
50 | console.log(' [-] Create Database:', DATABASE);
51 | return r.dbCreate(DATABASE).run(conn);
52 | }
53 |
54 | function createTable(conn, table) {
55 | console.log(' [-] Create Table:', table);
56 | return r.db(DATABASE).tableCreate(table).run(conn);
57 | }
58 |
59 | function closeConnection(conn) {
60 | console.log(' [x] Close connection!');
61 | return conn.close();
62 | }
--------------------------------------------------------------------------------
/images/favicon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GordyD/3ree/d4c2f3c250032350a07732ca66a6943a565c8690/images/favicon.gif
--------------------------------------------------------------------------------
/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GordyD/3ree/d4c2f3c250032350a07732ca66a6943a565c8690/images/favicon.ico
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpackConfig = require('./webpack.config.js');
2 |
3 | module.exports = function (config) {
4 | config.set({
5 | browsers: [ 'PhantomJS' ],
6 | captureTimeout: 60000,
7 | browserNoActivityTimeout: 60000, // We need to accept that Webpack may take a while to build!
8 | singleRun: true,
9 | colors: true,
10 | frameworks: [ 'mocha', 'sinon', 'chai' ], // Mocha is our testing framework of choice
11 | files: [
12 | './tests.webpack.js',
13 | ],
14 | preprocessors: {
15 | 'tests.webpack.js': [ 'webpack' ] // Preprocess with webpack and our sourcemap loader
16 | },
17 | reporters: [ 'mocha' ],
18 | webpack: { // Simplified Webpack configuration
19 | entry: webpackConfig.entry,
20 | module: {
21 | rules: webpackConfig.module.rules,
22 | noParse: [
23 | /node_modules\/sinon/,
24 | ]
25 | },
26 | node: {
27 | fs: 'empty'
28 | }
29 | },
30 | webpackServer: {
31 | noInfo: true // We don't want webpack output
32 | }
33 | });
34 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "3ree",
3 | "version": "1.0.0",
4 | "description": "An example universal JS application written with the 3REE stack, React + Redux + RethinkDB + Express.",
5 | "main": "server.babel.js",
6 | "scripts": {
7 | "build:prod": "NODE_ENV=production webpack --display-chunks",
8 | "db-setup": "node dbSetup.babel.js",
9 | "lint": "eslint client server universal test server.js dbSetup.js",
10 | "start": "NODE_ENV=development node start-dev.js",
11 | "start:win": "set NODE_ENV=development&&node start-dev.js",
12 | "start:prod": "NODE_ENV=production node server.babel.js",
13 | "test": "./node_modules/karma/bin/karma start"
14 | },
15 | "keywords": [
16 | "react",
17 | "3ree",
18 | "rethinkdb",
19 | "redux",
20 | "webpack"
21 | ],
22 | "license": "MIT",
23 | "engines": {
24 | "node": ">=6.0.0",
25 | "npm": ">=3.8.6"
26 | },
27 | "dependencies": {
28 | "babel": "6.23.x",
29 | "babel-core": "6.23.x",
30 | "babel-eslint": "7.1.1",
31 | "babel-loader": "6.4.x",
32 | "babel-plugin-rewire": "1.0.0",
33 | "babel-plugin-system-import-transformer": "^3.1.0",
34 | "babel-polyfill": "6.23.x",
35 | "babel-preset-es2015": "6.24.x",
36 | "babel-preset-react": "6.23.x",
37 | "babel-preset-react-hmre": "1.1.x",
38 | "babel-preset-stage-0": "6.22.x",
39 | "babel-runtime": "6.23.x",
40 | "babel-template": "6.23.x",
41 | "bluebird": "3.1.x",
42 | "body-parser": "1.14.x",
43 | "classnames": "2.2.x",
44 | "compression": "^1.6.2",
45 | "config": "1.19.x",
46 | "date-fns": "^1.28.2",
47 | "ejs": "2.3.x",
48 | "express": "4.13.x",
49 | "history": "1.13.x",
50 | "nib": "^1.1.x",
51 | "react": "15.0.x",
52 | "react-dom": "15.0.x",
53 | "react-redux": "4.4.x",
54 | "react-router": "2.4.x",
55 | "react-router-redux": "4.0.x",
56 | "redux": "3.5.x",
57 | "redux-logger": "2.6.x",
58 | "redux-thunk": "2.1.x",
59 | "rethinkdb": "2.3.x",
60 | "serve-static": "1.10.x",
61 | "socket.io": "1.4.x",
62 | "socket.io-client": "1.4.x",
63 | "superagent": "1.8.x",
64 | "xss": "0.2.x"
65 | },
66 | "devDependencies": {
67 | "chai": "3.5.x",
68 | "css-loader": "0.23.x",
69 | "eslint": "2.9.x",
70 | "eslint-plugin-react": "5.1.x",
71 | "extract-text-webpack-plugin": "2.1.x",
72 | "karma": "0.13.x",
73 | "karma-chai": "0.1.x",
74 | "karma-chrome-launcher": "1.0.x",
75 | "karma-mocha": "1.0.x",
76 | "karma-mocha-reporter": "2.0.x",
77 | "karma-phantomjs-launcher": "1.0.x",
78 | "karma-sinon": "1.0.x",
79 | "karma-webpack": "1.7.x",
80 | "mocha": "2.4.x",
81 | "mocha-loader": "0.7.x",
82 | "nock": "8.0.x",
83 | "node-libs-browser": "1.0.x",
84 | "phantomjs-prebuilt": "2.1.x",
85 | "raw-loader": "0.5.x",
86 | "react-hot-loader": "1.3.x",
87 | "react-transform-hmr": "1.0.x",
88 | "redux-devtools": "3.3.x",
89 | "redux-devtools-dock-monitor": "1.1.x",
90 | "redux-devtools-log-monitor": "1.0.x",
91 | "redux-mock-store": "1.0.x",
92 | "sinon": "1.17.x",
93 | "style-loader": "0.13.x",
94 | "stylus": "0.54.x",
95 | "stylus-loader": "2.5.x",
96 | "webpack": "2.2.x",
97 | "webpack-dev-server": "2.2.x"
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/server.babel.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('babel-polyfill');
3 | require('./server.js');
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import bodyParser from 'body-parser';
3 | import express from 'express';
4 | import compress from 'compression';
5 | import http from 'http';
6 | import socketIO from 'socket.io';
7 | import config from 'config';
8 |
9 | import * as api from './server/api/http';
10 | import * as eventService from './server/api/service/event';
11 | import * as uni from './server/app.js';
12 |
13 | const app = express();
14 | const httpServer = http.createServer(app);
15 | const port = config.get('express.port') || 3000;
16 |
17 | var io = socketIO(httpServer);
18 |
19 | app.set('views', path.join(__dirname, 'server', 'views'));
20 | app.set('view engine', 'ejs');
21 |
22 | /**
23 | * Server middleware
24 | */
25 | app.use(compress());
26 | app.use(require('serve-static')(path.join(__dirname, config.get('buildDirectory'))));
27 | app.use(bodyParser.urlencoded({
28 | extended: true
29 | }));
30 | app.use(bodyParser.json());
31 |
32 | /**
33 | * API Endpoints
34 | */
35 | app.get('/api/0/events', api.getEvents);
36 | app.post('/api/0/events', api.addEvent);
37 | app.post('/api/0/events/:id', api.editEvent);
38 | app.delete('/api/0/events/:id', api.deleteEvent);
39 |
40 | app.get('/favicon.ico', (req, res) => res.sendFile(path.join(__dirname, 'images', 'favicon.ico')));
41 |
42 | /**
43 | * Universal Application endpoint
44 | */
45 | app.get('*', uni.handleRender);
46 |
47 | eventService.liveUpdates(io);
48 |
49 | httpServer.listen(port);
--------------------------------------------------------------------------------
/server.webpack.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development';
2 |
3 | var webpack = require('webpack');
4 | var WebpackDevServer = require('webpack-dev-server');
5 |
6 | var config = require('config');
7 | var webpackConfig = require('./webpack.config');
8 |
9 | var host = 'localhost';
10 | var appPort = 3000;
11 | var devServerPort = 3001;
12 |
13 | new WebpackDevServer(webpack(webpackConfig), {
14 | contentBase: [ config.get('buildDirectory'), '/' ].join(''),
15 | headers: { 'Access-Control-Allow-Origin': '*' },
16 | historyApiFallback: true,
17 | hot: true,
18 | noInfo: false,
19 | publicPath: webpackConfig.output.publicPath,
20 | proxy: {
21 | '*': 'http://' + host + ':' + appPort
22 | }
23 | }).listen(devServerPort, host, function (err) {
24 | if (err) {
25 | console.log(err);
26 | }
27 |
28 | console.log('Webpack Dev Server running at ' + host + ':' + devServerPort);
29 | });
30 |
--------------------------------------------------------------------------------
/server/api/http.js:
--------------------------------------------------------------------------------
1 | import * as service from './service/event';
2 |
3 | export function getEvents(req, res) {
4 | service.getEvents()
5 | .then((events) => res.json(events))
6 | .catch(err => {
7 | res.status(400);
8 | res.json({error: err});
9 | });
10 | }
11 |
12 | export function addEvent(req, res) {
13 | service.addEvent(req.body)
14 | .then((event) => res.json(event))
15 | .catch(err => {
16 | res.status(400);
17 | res.json({error: err, event: req.body});
18 | });
19 | }
20 |
21 | export function editEvent(req, res) {
22 | service.editEvent(req.params.id, req.body)
23 | .then((event) => res.json(event))
24 | .catch(err => {
25 | res.status(400);
26 | res.json({error: err, event: req.body});
27 | });
28 | }
29 |
30 | export function deleteEvent(req, res) {
31 | service.deleteEvent(req.params.id)
32 | .then((event) => res.json(event))
33 | .catch(err => {
34 | res.status(400);
35 | res.json({error: err, event: req.body});
36 | });
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/server/api/service/event.js:
--------------------------------------------------------------------------------
1 | import r from 'rethinkdb';
2 | import config from 'config';
3 | import xss from 'xss';
4 |
5 | function connect() {
6 | return r.connect(config.get('rethinkdb'));
7 | }
8 |
9 | export function liveUpdates(io) {
10 | console.log('Setting up listener...');
11 | connect()
12 | .then(conn => {
13 | r
14 | .table('pulses')
15 | .changes().run(conn, (err, cursor) => {
16 | console.log('Listening for changes...');
17 | cursor.each((err, change) => {
18 | console.log('Change detected', change);
19 | io.emit('event-change', change);
20 | });
21 | });
22 | });
23 | }
24 |
25 | export function getEvents() {
26 | return connect()
27 | .then(conn => {
28 | return r
29 | .table('pulses')
30 | .orderBy(r.desc('created')).run(conn)
31 | .then(cursor => cursor.toArray());
32 | });
33 | }
34 |
35 | export function addEvent(event) {
36 | return connect()
37 | .then(conn => {
38 | event.created = new Date();
39 | event.text = xss(event.text);
40 | return r
41 | .table('pulses')
42 | .insert(event).run(conn)
43 | .then(response => {
44 | return Object.assign({}, event, {id: response.generated_keys[0]});
45 | });
46 | });
47 | }
48 |
49 | export function editEvent(id, event) {
50 | event.updated = new Date();
51 | event.text = xss(event.text);
52 | return connect()
53 | .then(conn => {
54 | return r
55 | .table('pulses')
56 | .get(id).update(event).run(conn)
57 | .then(() => event);
58 | });
59 | }
60 |
61 | export function deleteEvent(id) {
62 | return connect()
63 | .then(conn => {
64 | return r
65 | .table('pulses')
66 | .get(id).delete().run(conn)
67 | .then(() => ({id: id, deleted: true}));
68 | });
69 | }
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOMServer from 'react-dom/server';
3 | import { Provider } from 'react-redux';
4 | import { RouterContext, match } from 'react-router';
5 |
6 | import * as eventService from './api/service/event';
7 | import configureStore from '../universal/store';
8 | import routes from '../universal/routes';
9 | import DevTools from '../universal/containers/devTools';
10 |
11 | const isDev = (process.env.NODE_ENV !== 'production');
12 |
13 | export function handleRender(req, res) {
14 | console.log(' [x] Request for', req.url);
15 | eventService.getEvents()
16 | .then(initialEvents => {
17 | let initialState = {pulseApp: { events: initialEvents, userId: 'baseUser'} };
18 |
19 | const store = configureStore(req, initialState);
20 |
21 | // Wire up routing based upon routes
22 | match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
23 | if (error || !renderProps) {
24 |
25 | if (req.url === '/bundle.js') {
26 | console.log(' | Hold up, are you sure you are hitting the app at http://localhost:3001?');
27 | console.log(' | On development bundle.js is served by the Webpack Dev Server and so you need to hit the app on port 3001, not port 3000.');
28 | }
29 | console.log((error) ? error : 'Error: No matching universal route found');
30 |
31 | res.status(400);
32 | res.send((error) ? error : 'Error: No matching universal route found');
33 | return;
34 | }
35 |
36 | if (redirectLocation) {
37 | res.redirect(redirectLocation);
38 | return;
39 | }
40 |
41 | const devTools = (isDev) ? : null;
42 |
43 | // Render the component to a string
44 | const html = ReactDOMServer.renderToString(
45 |
46 |
47 |
48 | {devTools}
49 |
50 |
51 | );
52 |
53 | // Send the rendered page back to the client with the initial state
54 | res.render('index', { isProd: (!isDev), html: html, initialState: JSON.stringify(store.getState()) });
55 | });
56 | });
57 | }
--------------------------------------------------------------------------------
/server/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Pulse Universal
5 |
6 | <% if (isProd) { %>
7 |
8 | <% } %>
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
20 |
22 |
23 |
--------------------------------------------------------------------------------
/start-dev.js:
--------------------------------------------------------------------------------
1 | require('./server.babel.js');
2 | require('./server.webpack.js');
3 |
--------------------------------------------------------------------------------
/style/main.styl:
--------------------------------------------------------------------------------
1 | body
2 | background-color: #fff
3 |
4 | h1, h2, h3, p
5 | margin: 0px
6 |
7 | *
8 | font-family: 'Source Sans Pro', sans-serif
9 |
10 | .Pulse-header
11 | background-color: #252A3A
12 | color: #1B98F8
13 | margin-bottom: 20px
14 | box-shadow: 0px 3px 12px #4B7197
15 |
16 | h1
17 | padding: 20px
18 | text-transform: uppercase
19 | text-align: center
20 |
21 | .Pulse-links
22 | padding: 10px
23 | text-align: center
24 |
25 | a
26 | padding: 10px
27 | color: #fff
28 | text-decoration: none
29 |
30 | &:hover
31 | color: #cccccc
32 |
33 | &.active
34 | text-decoration: underline
35 |
36 | .Pulse-addEventForm, .Pulse-eventList, .Pulse-async
37 | width: 600px
38 | margin: 0 auto
39 | padding-bottom: 20px
40 |
41 | .Pulse-async
42 | text-align: center
43 | min-height: 30px
44 |
45 | .Pulse-async-error
46 | color: #EE5656
47 | font-weight: bold
48 |
49 | .Pulse-addEventForm
50 | form
51 | text-align: center
52 |
53 | form label
54 | color: #4B7197
55 | margin-left: 10px
56 |
57 | .Pulse-eventInput-value
58 | display: inline-block
59 | color: #4B7197
60 | margin-right: 15px
61 | font-weight: bold
62 | width: 40px
63 |
64 | .Pulse-eventList-summary
65 | padding-bottom: 10px
66 | color: #252A3A
67 |
68 | margin-bottom: 10px
69 | text-align: center
70 |
71 | span
72 | margin: 5px
73 |
74 | .val
75 | font-weight: 700
76 |
77 | .Pulse-eventInput
78 | input, button, label
79 | margin-right: 6px
80 |
81 | input[type=text]
82 | padding: 10px
83 | border: solid 1px #4B7197
84 | background-color: #fff
85 | color: #1B98F8
86 | appearance: none
87 | box-shadow: none
88 | transition: box-shadow 0.5s
89 |
90 | &:focus, &.focus
91 | box-shadow: 0 0 5px 1px #4B7197
92 |
93 | &::-webkit-input-placeholder
94 | color: #4B7197
95 |
96 | input[type=range]
97 | -webkit-appearance: none
98 | background-color: transparent
99 | vertical-align: middle
100 | margin: -3px 15px 0
101 |
102 | &::-webkit-slider-runnable-track
103 | width: 300px
104 | height: 3px
105 | background: #ddd
106 | border: none
107 | border-radius: 5px
108 |
109 | input[type=range]::-webkit-slider-thumb
110 | -webkit-appearance: none
111 | border: none
112 | height: 16px
113 | width: 16px
114 | border-radius: 50%
115 | background: rgba(27,152,148, 0.5)
116 | margin-top: -7px
117 |
118 | input.very-low[type=range]::-webkit-slider-thumb
119 | background: #84C8FB
120 | input.low[type=range]::-webkit-slider-thumb
121 | background: #6ABCFA
122 | input.mid[type=range]::-webkit-slider-thumb
123 | background: #4FB0F9
124 | input.high[type=range]::-webkit-slider-thumb
125 | background: #35A4F8
126 | input.very-high[type=range]::-webkit-slider-thumb
127 | background: #1B98F8
128 |
129 | input[type=range]:focus
130 | outline: none
131 |
132 | input[type=range]:focus::-webkit-slider-runnable-track
133 | background: #ccc
134 |
135 | button
136 | background-color: #1B98F8
137 | color: #fff
138 |
139 | .Pulse-eventList-list
140 | ul
141 | list-style-type: none
142 | margin: 0
143 | padding: 0
144 | li
145 | padding: 10px 20px
146 | background-color: #F8F8F8
147 |
148 | li:first-child
149 | border-top-left-radius: 10px
150 | border-top-right-radius: 10px
151 |
152 | li:last-child
153 | border-bottom-left-radius: 10px
154 | border-bottom-right-radius: 10px
155 | box-shadow: 0 3px 12px -8px #4B7197
156 |
157 | li.odd
158 | background-color: #F2F2F2
159 |
160 | form
161 | margin: 0
162 |
163 | fieldset
164 | padding: 0
165 |
166 | .Pulse-eventItem
167 | p
168 | margin-top: 6px
169 |
170 | &.empty
171 | p
172 | margin-top: 0px
173 | text-align: center
174 | color: #cdcdcd
175 |
176 | &.rowNumber
177 | float: left
178 | color: #cdcdcd
179 | width: 20px
180 | padding-right: 5px
181 |
182 | p.title
183 | float: left
184 | color: #4B7197
185 | cursor: pointer
186 |
187 | p.title:hover
188 | text-decoration: underline
189 |
190 | p.outcome, p.created
191 | float: right
192 | padding-right: 15px
193 | color: #cdcdcd
194 |
195 | p.outcom
196 | font-weight: bold
197 | color: #252A3A
198 |
199 | .destroy
200 | background-color: #EE5656
201 | color: #fff
202 |
203 | &::before
204 | content: 'x'
205 |
206 | .destroy, .save
207 | float: right
208 | margin-right: 0
209 |
210 | &::after
211 | clear: both
212 | content: '.'
213 | display: block
214 | height: 0
215 | visibility: hidden
--------------------------------------------------------------------------------
/style/pure.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Pure v0.6.0
3 | Copyright 2014 Yahoo! Inc. All rights reserved.
4 | Licensed under the BSD License.
5 | https://github.com/yahoo/pure/blob/master/LICENSE.md
6 | */
7 | /*!
8 | normalize.css v^3.0 | MIT License | git.io/normalize
9 | Copyright (c) Nicolas Gallagher and Jonathan Neal
10 | */
11 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
12 |
13 | /**
14 | * 1. Set default font family to sans-serif.
15 | * 2. Prevent iOS text size adjust after orientation change, without disabling
16 | * user zoom.
17 | */
18 |
19 | html {
20 | font-family: sans-serif; /* 1 */
21 | -ms-text-size-adjust: 100%; /* 2 */
22 | -webkit-text-size-adjust: 100%; /* 2 */
23 | }
24 |
25 | /**
26 | * Remove default margin.
27 | */
28 |
29 | body {
30 | margin: 0;
31 | }
32 |
33 | /* HTML5 display definitions
34 | ========================================================================== */
35 |
36 | /**
37 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
38 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
39 | * and Firefox.
40 | * Correct `block` display not defined for `main` in IE 11.
41 | */
42 |
43 | article,
44 | aside,
45 | details,
46 | figcaption,
47 | figure,
48 | footer,
49 | header,
50 | hgroup,
51 | main,
52 | menu,
53 | nav,
54 | section,
55 | summary {
56 | display: block;
57 | }
58 |
59 | /**
60 | * 1. Correct `inline-block` display not defined in IE 8/9.
61 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
62 | */
63 |
64 | audio,
65 | canvas,
66 | progress,
67 | video {
68 | display: inline-block; /* 1 */
69 | vertical-align: baseline; /* 2 */
70 | }
71 |
72 | /**
73 | * Prevent modern browsers from displaying `audio` without controls.
74 | * Remove excess height in iOS 5 devices.
75 | */
76 |
77 | audio:not([controls]) {
78 | display: none;
79 | height: 0;
80 | }
81 |
82 | /**
83 | * Address `[hidden]` styling not present in IE 8/9/10.
84 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
85 | */
86 |
87 | [hidden],
88 | template {
89 | display: none;
90 | }
91 |
92 | /* Links
93 | ========================================================================== */
94 |
95 | /**
96 | * Remove the gray background color from active links in IE 10.
97 | */
98 |
99 | a {
100 | background-color: transparent;
101 | }
102 |
103 | /**
104 | * Improve readability when focused and also mouse hovered in all browsers.
105 | */
106 |
107 | a:active,
108 | a:hover {
109 | outline: 0;
110 | }
111 |
112 | /* Text-level semantics
113 | ========================================================================== */
114 |
115 | /**
116 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
117 | */
118 |
119 | abbr[title] {
120 | border-bottom: 1px dotted;
121 | }
122 |
123 | /**
124 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
125 | */
126 |
127 | b,
128 | strong {
129 | font-weight: bold;
130 | }
131 |
132 | /**
133 | * Address styling not present in Safari and Chrome.
134 | */
135 |
136 | dfn {
137 | font-style: italic;
138 | }
139 |
140 | /**
141 | * Address variable `h1` font-size and margin within `section` and `article`
142 | * contexts in Firefox 4+, Safari, and Chrome.
143 | */
144 |
145 | h1 {
146 | font-size: 2em;
147 | margin: 0.67em 0;
148 | }
149 |
150 | /**
151 | * Address styling not present in IE 8/9.
152 | */
153 |
154 | mark {
155 | background: #ff0;
156 | color: #000;
157 | }
158 |
159 | /**
160 | * Address inconsistent and variable font size in all browsers.
161 | */
162 |
163 | small {
164 | font-size: 80%;
165 | }
166 |
167 | /**
168 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
169 | */
170 |
171 | sub,
172 | sup {
173 | font-size: 75%;
174 | line-height: 0;
175 | position: relative;
176 | vertical-align: baseline;
177 | }
178 |
179 | sup {
180 | top: -0.5em;
181 | }
182 |
183 | sub {
184 | bottom: -0.25em;
185 | }
186 |
187 | /* Embedded content
188 | ========================================================================== */
189 |
190 | /**
191 | * Remove border when inside `a` element in IE 8/9/10.
192 | */
193 |
194 | img {
195 | border: 0;
196 | }
197 |
198 | /**
199 | * Correct overflow not hidden in IE 9/10/11.
200 | */
201 |
202 | svg:not(:root) {
203 | overflow: hidden;
204 | }
205 |
206 | /* Grouping content
207 | ========================================================================== */
208 |
209 | /**
210 | * Address margin not present in IE 8/9 and Safari.
211 | */
212 |
213 | figure {
214 | margin: 1em 40px;
215 | }
216 |
217 | /**
218 | * Address differences between Firefox and other browsers.
219 | */
220 |
221 | hr {
222 | -moz-box-sizing: content-box;
223 | box-sizing: content-box;
224 | height: 0;
225 | }
226 |
227 | /**
228 | * Contain overflow in all browsers.
229 | */
230 |
231 | pre {
232 | overflow: auto;
233 | }
234 |
235 | /**
236 | * Address odd `em`-unit font size rendering in all browsers.
237 | */
238 |
239 | code,
240 | kbd,
241 | pre,
242 | samp {
243 | font-family: monospace, monospace;
244 | font-size: 1em;
245 | }
246 |
247 | /* Forms
248 | ========================================================================== */
249 |
250 | /**
251 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
252 | * styling of `select`, unless a `border` property is set.
253 | */
254 |
255 | /**
256 | * 1. Correct color not being inherited.
257 | * Known issue: affects color of disabled elements.
258 | * 2. Correct font properties not being inherited.
259 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
260 | */
261 |
262 | button,
263 | input,
264 | optgroup,
265 | select,
266 | textarea {
267 | color: inherit; /* 1 */
268 | font: inherit; /* 2 */
269 | margin: 0; /* 3 */
270 | }
271 |
272 | /**
273 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
274 | */
275 |
276 | button {
277 | overflow: visible;
278 | }
279 |
280 | /**
281 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
282 | * All other form control elements do not inherit `text-transform` values.
283 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
284 | * Correct `select` style inheritance in Firefox.
285 | */
286 |
287 | button,
288 | select {
289 | text-transform: none;
290 | }
291 |
292 | /**
293 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
294 | * and `video` controls.
295 | * 2. Correct inability to style clickable `input` types in iOS.
296 | * 3. Improve usability and consistency of cursor style between image-type
297 | * `input` and others.
298 | */
299 |
300 | button,
301 | html input[type="button"], /* 1 */
302 | input[type="reset"],
303 | input[type="submit"] {
304 | -webkit-appearance: button; /* 2 */
305 | cursor: pointer; /* 3 */
306 | }
307 |
308 | /**
309 | * Re-set default cursor for disabled elements.
310 | */
311 |
312 | button[disabled],
313 | html input[disabled] {
314 | cursor: default;
315 | }
316 |
317 | /**
318 | * Remove inner padding and border in Firefox 4+.
319 | */
320 |
321 | button::-moz-focus-inner,
322 | input::-moz-focus-inner {
323 | border: 0;
324 | padding: 0;
325 | }
326 |
327 | /**
328 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
329 | * the UA stylesheet.
330 | */
331 |
332 | input {
333 | line-height: normal;
334 | }
335 |
336 | /**
337 | * It's recommended that you don't attempt to style these elements.
338 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
339 | *
340 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
341 | * 2. Remove excess padding in IE 8/9/10.
342 | */
343 |
344 | input[type="checkbox"],
345 | input[type="radio"] {
346 | box-sizing: border-box; /* 1 */
347 | padding: 0; /* 2 */
348 | }
349 |
350 | /**
351 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
352 | * `font-size` values of the `input`, it causes the cursor style of the
353 | * decrement button to change from `default` to `text`.
354 | */
355 |
356 | input[type="number"]::-webkit-inner-spin-button,
357 | input[type="number"]::-webkit-outer-spin-button {
358 | height: auto;
359 | }
360 |
361 | /**
362 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
363 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
364 | * (include `-moz` to future-proof).
365 | */
366 |
367 | input[type="search"] {
368 | -webkit-appearance: textfield; /* 1 */
369 | -moz-box-sizing: content-box;
370 | -webkit-box-sizing: content-box; /* 2 */
371 | box-sizing: content-box;
372 | }
373 |
374 | /**
375 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
376 | * Safari (but not Chrome) clips the cancel button when the search input has
377 | * padding (and `textfield` appearance).
378 | */
379 |
380 | input[type="search"]::-webkit-search-cancel-button,
381 | input[type="search"]::-webkit-search-decoration {
382 | -webkit-appearance: none;
383 | }
384 |
385 | /**
386 | * Define consistent border, margin, and padding.
387 | */
388 |
389 | fieldset {
390 | border: 1px solid #c0c0c0;
391 | margin: 0 2px;
392 | padding: 0.35em 0.625em 0.75em;
393 | }
394 |
395 | /**
396 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
397 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
398 | */
399 |
400 | legend {
401 | border: 0; /* 1 */
402 | padding: 0; /* 2 */
403 | }
404 |
405 | /**
406 | * Remove default vertical scrollbar in IE 8/9/10/11.
407 | */
408 |
409 | textarea {
410 | overflow: auto;
411 | }
412 |
413 | /**
414 | * Don't inherit the `font-weight` (applied by a rule above).
415 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
416 | */
417 |
418 | optgroup {
419 | font-weight: bold;
420 | }
421 |
422 | /* Tables
423 | ========================================================================== */
424 |
425 | /**
426 | * Remove most spacing between table cells.
427 | */
428 |
429 | table {
430 | border-collapse: collapse;
431 | border-spacing: 0;
432 | }
433 |
434 | td,
435 | th {
436 | padding: 0;
437 | }
438 |
439 | /*csslint important:false*/
440 |
441 | /* ==========================================================================
442 | Pure Base Extras
443 | ========================================================================== */
444 |
445 | /**
446 | * Extra rules that Pure adds on top of Normalize.css
447 | */
448 |
449 | /**
450 | * Always hide an element when it has the `hidden` HTML attribute.
451 | */
452 |
453 | .hidden,
454 | [hidden] {
455 | display: none !important;
456 | }
457 |
458 | /**
459 | * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining
460 | * aspect ratio.
461 | */
462 | .pure-img {
463 | max-width: 100%;
464 | height: auto;
465 | display: block;
466 | }
467 |
468 | /*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
469 |
470 | .pure-g {
471 | letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
472 | *letter-spacing: normal; /* reset IE < 8 */
473 | *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */
474 | text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
475 |
476 | /*
477 | Sets the font stack to fonts known to work properly with the above letter
478 | and word spacings. See: https://github.com/yahoo/pure/issues/41/
479 |
480 | The following font stack makes Pure Grids work on all known environments.
481 |
482 | * FreeSans: Ships with many Linux distros, including Ubuntu
483 |
484 | * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
485 | Arial to get picked up by the browser, even though neither is available
486 | in Chrome OS.
487 |
488 | * Droid Sans: Ships with all versions of Android.
489 |
490 | * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
491 | */
492 | font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
493 |
494 | /*
495 | Use flexbox when possible to avoid `letter-spacing` side-effects.
496 |
497 | NOTE: Firefox (as of 25) does not currently support flex-wrap, so the
498 | `-moz-` prefix version is omitted.
499 | */
500 |
501 | display: -webkit-flex;
502 | -webkit-flex-flow: row wrap;
503 |
504 | /* IE10 uses display: flexbox */
505 | display: -ms-flexbox;
506 | -ms-flex-flow: row wrap;
507 |
508 | /* Prevents distributing space between rows */
509 | -ms-align-content: flex-start;
510 | -webkit-align-content: flex-start;
511 | align-content: flex-start;
512 | }
513 |
514 | /* Opera as of 12 on Windows needs word-spacing.
515 | The ".opera-only" selector is used to prevent actual prefocus styling
516 | and is not required in markup.
517 | */
518 | .opera-only :-o-prefocus,
519 | .pure-g {
520 | word-spacing: -0.43em;
521 | }
522 |
523 | .pure-u {
524 | display: inline-block;
525 | *display: inline; /* IE < 8: fake inline-block */
526 | zoom: 1;
527 | letter-spacing: normal;
528 | word-spacing: normal;
529 | vertical-align: top;
530 | text-rendering: auto;
531 | }
532 |
533 | /*
534 | Resets the font family back to the OS/browser's default sans-serif font,
535 | this the same font stack that Normalize.css sets for the `body`.
536 | */
537 | .pure-g [class *= "pure-u"] {
538 | font-family: sans-serif;
539 | }
540 |
541 | .pure-u-1,
542 | .pure-u-1-1,
543 | .pure-u-1-2,
544 | .pure-u-1-3,
545 | .pure-u-2-3,
546 | .pure-u-1-4,
547 | .pure-u-3-4,
548 | .pure-u-1-5,
549 | .pure-u-2-5,
550 | .pure-u-3-5,
551 | .pure-u-4-5,
552 | .pure-u-5-5,
553 | .pure-u-1-6,
554 | .pure-u-5-6,
555 | .pure-u-1-8,
556 | .pure-u-3-8,
557 | .pure-u-5-8,
558 | .pure-u-7-8,
559 | .pure-u-1-12,
560 | .pure-u-5-12,
561 | .pure-u-7-12,
562 | .pure-u-11-12,
563 | .pure-u-1-24,
564 | .pure-u-2-24,
565 | .pure-u-3-24,
566 | .pure-u-4-24,
567 | .pure-u-5-24,
568 | .pure-u-6-24,
569 | .pure-u-7-24,
570 | .pure-u-8-24,
571 | .pure-u-9-24,
572 | .pure-u-10-24,
573 | .pure-u-11-24,
574 | .pure-u-12-24,
575 | .pure-u-13-24,
576 | .pure-u-14-24,
577 | .pure-u-15-24,
578 | .pure-u-16-24,
579 | .pure-u-17-24,
580 | .pure-u-18-24,
581 | .pure-u-19-24,
582 | .pure-u-20-24,
583 | .pure-u-21-24,
584 | .pure-u-22-24,
585 | .pure-u-23-24,
586 | .pure-u-24-24 {
587 | display: inline-block;
588 | *display: inline;
589 | zoom: 1;
590 | letter-spacing: normal;
591 | word-spacing: normal;
592 | vertical-align: top;
593 | text-rendering: auto;
594 | }
595 |
596 | .pure-u-1-24 {
597 | width: 4.1667%;
598 | *width: 4.1357%;
599 | }
600 |
601 | .pure-u-1-12,
602 | .pure-u-2-24 {
603 | width: 8.3333%;
604 | *width: 8.3023%;
605 | }
606 |
607 | .pure-u-1-8,
608 | .pure-u-3-24 {
609 | width: 12.5000%;
610 | *width: 12.4690%;
611 | }
612 |
613 | .pure-u-1-6,
614 | .pure-u-4-24 {
615 | width: 16.6667%;
616 | *width: 16.6357%;
617 | }
618 |
619 | .pure-u-1-5 {
620 | width: 20%;
621 | *width: 19.9690%;
622 | }
623 |
624 | .pure-u-5-24 {
625 | width: 20.8333%;
626 | *width: 20.8023%;
627 | }
628 |
629 | .pure-u-1-4,
630 | .pure-u-6-24 {
631 | width: 25%;
632 | *width: 24.9690%;
633 | }
634 |
635 | .pure-u-7-24 {
636 | width: 29.1667%;
637 | *width: 29.1357%;
638 | }
639 |
640 | .pure-u-1-3,
641 | .pure-u-8-24 {
642 | width: 33.3333%;
643 | *width: 33.3023%;
644 | }
645 |
646 | .pure-u-3-8,
647 | .pure-u-9-24 {
648 | width: 37.5000%;
649 | *width: 37.4690%;
650 | }
651 |
652 | .pure-u-2-5 {
653 | width: 40%;
654 | *width: 39.9690%;
655 | }
656 |
657 | .pure-u-5-12,
658 | .pure-u-10-24 {
659 | width: 41.6667%;
660 | *width: 41.6357%;
661 | }
662 |
663 | .pure-u-11-24 {
664 | width: 45.8333%;
665 | *width: 45.8023%;
666 | }
667 |
668 | .pure-u-1-2,
669 | .pure-u-12-24 {
670 | width: 50%;
671 | *width: 49.9690%;
672 | }
673 |
674 | .pure-u-13-24 {
675 | width: 54.1667%;
676 | *width: 54.1357%;
677 | }
678 |
679 | .pure-u-7-12,
680 | .pure-u-14-24 {
681 | width: 58.3333%;
682 | *width: 58.3023%;
683 | }
684 |
685 | .pure-u-3-5 {
686 | width: 60%;
687 | *width: 59.9690%;
688 | }
689 |
690 | .pure-u-5-8,
691 | .pure-u-15-24 {
692 | width: 62.5000%;
693 | *width: 62.4690%;
694 | }
695 |
696 | .pure-u-2-3,
697 | .pure-u-16-24 {
698 | width: 66.6667%;
699 | *width: 66.6357%;
700 | }
701 |
702 | .pure-u-17-24 {
703 | width: 70.8333%;
704 | *width: 70.8023%;
705 | }
706 |
707 | .pure-u-3-4,
708 | .pure-u-18-24 {
709 | width: 75%;
710 | *width: 74.9690%;
711 | }
712 |
713 | .pure-u-19-24 {
714 | width: 79.1667%;
715 | *width: 79.1357%;
716 | }
717 |
718 | .pure-u-4-5 {
719 | width: 80%;
720 | *width: 79.9690%;
721 | }
722 |
723 | .pure-u-5-6,
724 | .pure-u-20-24 {
725 | width: 83.3333%;
726 | *width: 83.3023%;
727 | }
728 |
729 | .pure-u-7-8,
730 | .pure-u-21-24 {
731 | width: 87.5000%;
732 | *width: 87.4690%;
733 | }
734 |
735 | .pure-u-11-12,
736 | .pure-u-22-24 {
737 | width: 91.6667%;
738 | *width: 91.6357%;
739 | }
740 |
741 | .pure-u-23-24 {
742 | width: 95.8333%;
743 | *width: 95.8023%;
744 | }
745 |
746 | .pure-u-1,
747 | .pure-u-1-1,
748 | .pure-u-5-5,
749 | .pure-u-24-24 {
750 | width: 100%;
751 | }
752 | .pure-button {
753 | /* Structure */
754 | display: inline-block;
755 | zoom: 1;
756 | line-height: normal;
757 | white-space: nowrap;
758 | vertical-align: middle;
759 | text-align: center;
760 | cursor: pointer;
761 | -webkit-user-drag: none;
762 | -webkit-user-select: none;
763 | -moz-user-select: none;
764 | -ms-user-select: none;
765 | user-select: none;
766 | -webkit-box-sizing: border-box;
767 | -moz-box-sizing: border-box;
768 | box-sizing: border-box;
769 | }
770 |
771 | /* Firefox: Get rid of the inner focus border */
772 | .pure-button::-moz-focus-inner {
773 | padding: 0;
774 | border: 0;
775 | }
776 |
777 | /*csslint outline-none:false*/
778 |
779 | .pure-button {
780 | font-family: inherit;
781 | font-size: 100%;
782 | padding: 0.5em 1em;
783 | color: #444; /* rgba not supported (IE 8) */
784 | color: rgba(0, 0, 0, 0.80); /* rgba supported */
785 | border: 1px solid #999; /*IE 6/7/8*/
786 | border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
787 | background-color: #E6E6E6;
788 | text-decoration: none;
789 | border-radius: 2px;
790 | }
791 |
792 | .pure-button-hover,
793 | .pure-button:hover,
794 | .pure-button:focus {
795 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000',GradientType=0);
796 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10)));
797 | background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10));
798 | background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10));
799 | background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10));
800 | background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10));
801 | }
802 | .pure-button:focus {
803 | outline: 0;
804 | }
805 | .pure-button-active,
806 | .pure-button:active {
807 | box-shadow: 0 0 0 1px rgba(0,0,0, 0.15) inset, 0 0 6px rgba(0,0,0, 0.20) inset;
808 | border-color: #000\9;
809 | }
810 |
811 | .pure-button[disabled],
812 | .pure-button-disabled,
813 | .pure-button-disabled:hover,
814 | .pure-button-disabled:focus,
815 | .pure-button-disabled:active {
816 | border: none;
817 | background-image: none;
818 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
819 | filter: alpha(opacity=40);
820 | -khtml-opacity: 0.40;
821 | -moz-opacity: 0.40;
822 | opacity: 0.40;
823 | cursor: not-allowed;
824 | box-shadow: none;
825 | }
826 |
827 | .pure-button-hidden {
828 | display: none;
829 | }
830 |
831 | /* Firefox: Get rid of the inner focus border */
832 | .pure-button::-moz-focus-inner{
833 | padding: 0;
834 | border: 0;
835 | }
836 |
837 | .pure-button-primary,
838 | .pure-button-selected,
839 | a.pure-button-primary,
840 | a.pure-button-selected {
841 | background-color: rgb(0, 120, 231);
842 | color: #fff;
843 | }
844 |
845 | /*csslint box-model:false*/
846 | /*
847 | Box-model set to false because we're setting a height on select elements, which
848 | also have border and padding. This is done because some browsers don't render
849 | the padding. We explicitly set the box-model for select elements to border-box,
850 | so we can ignore the csslint warning.
851 | */
852 |
853 | .pure-form input[type="text"],
854 | .pure-form input[type="password"],
855 | .pure-form input[type="email"],
856 | .pure-form input[type="url"],
857 | .pure-form input[type="date"],
858 | .pure-form input[type="month"],
859 | .pure-form input[type="time"],
860 | .pure-form input[type="datetime"],
861 | .pure-form input[type="datetime-local"],
862 | .pure-form input[type="week"],
863 | .pure-form input[type="number"],
864 | .pure-form input[type="search"],
865 | .pure-form input[type="tel"],
866 | .pure-form input[type="color"],
867 | .pure-form select,
868 | .pure-form textarea {
869 | padding: 0.5em 0.6em;
870 | display: inline-block;
871 | border: 1px solid #ccc;
872 | box-shadow: inset 0 1px 3px #ddd;
873 | border-radius: 4px;
874 | vertical-align: middle;
875 | -webkit-box-sizing: border-box;
876 | -moz-box-sizing: border-box;
877 | box-sizing: border-box;
878 | }
879 |
880 | /*
881 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
882 | since IE8 won't execute CSS that contains a CSS3 selector.
883 | */
884 | .pure-form input:not([type]) {
885 | padding: 0.5em 0.6em;
886 | display: inline-block;
887 | border: 1px solid #ccc;
888 | box-shadow: inset 0 1px 3px #ddd;
889 | border-radius: 4px;
890 | -webkit-box-sizing: border-box;
891 | -moz-box-sizing: border-box;
892 | box-sizing: border-box;
893 | }
894 |
895 |
896 | /* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */
897 | /* May be able to remove this tweak as color inputs become more standardized across browsers. */
898 | .pure-form input[type="color"] {
899 | padding: 0.2em 0.5em;
900 | }
901 |
902 |
903 | .pure-form input[type="text"]:focus,
904 | .pure-form input[type="password"]:focus,
905 | .pure-form input[type="email"]:focus,
906 | .pure-form input[type="url"]:focus,
907 | .pure-form input[type="date"]:focus,
908 | .pure-form input[type="month"]:focus,
909 | .pure-form input[type="time"]:focus,
910 | .pure-form input[type="datetime"]:focus,
911 | .pure-form input[type="datetime-local"]:focus,
912 | .pure-form input[type="week"]:focus,
913 | .pure-form input[type="number"]:focus,
914 | .pure-form input[type="search"]:focus,
915 | .pure-form input[type="tel"]:focus,
916 | .pure-form input[type="color"]:focus,
917 | .pure-form select:focus,
918 | .pure-form textarea:focus {
919 | outline: 0;
920 | border-color: #129FEA;
921 | }
922 |
923 | /*
924 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
925 | since IE8 won't execute CSS that contains a CSS3 selector.
926 | */
927 | .pure-form input:not([type]):focus {
928 | outline: 0;
929 | border-color: #129FEA;
930 | }
931 |
932 | .pure-form input[type="file"]:focus,
933 | .pure-form input[type="radio"]:focus,
934 | .pure-form input[type="checkbox"]:focus {
935 | outline: thin solid #129FEA;
936 | outline: 1px auto #129FEA;
937 | }
938 | .pure-form .pure-checkbox,
939 | .pure-form .pure-radio {
940 | margin: 0.5em 0;
941 | display: block;
942 | }
943 |
944 | .pure-form input[type="text"][disabled],
945 | .pure-form input[type="password"][disabled],
946 | .pure-form input[type="email"][disabled],
947 | .pure-form input[type="url"][disabled],
948 | .pure-form input[type="date"][disabled],
949 | .pure-form input[type="month"][disabled],
950 | .pure-form input[type="time"][disabled],
951 | .pure-form input[type="datetime"][disabled],
952 | .pure-form input[type="datetime-local"][disabled],
953 | .pure-form input[type="week"][disabled],
954 | .pure-form input[type="number"][disabled],
955 | .pure-form input[type="search"][disabled],
956 | .pure-form input[type="tel"][disabled],
957 | .pure-form input[type="color"][disabled],
958 | .pure-form select[disabled],
959 | .pure-form textarea[disabled] {
960 | cursor: not-allowed;
961 | background-color: #eaeded;
962 | color: #cad2d3;
963 | }
964 |
965 | /*
966 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
967 | since IE8 won't execute CSS that contains a CSS3 selector.
968 | */
969 | .pure-form input:not([type])[disabled] {
970 | cursor: not-allowed;
971 | background-color: #eaeded;
972 | color: #cad2d3;
973 | }
974 | .pure-form input[readonly],
975 | .pure-form select[readonly],
976 | .pure-form textarea[readonly] {
977 | background-color: #eee; /* menu hover bg color */
978 | color: #777; /* menu text color */
979 | border-color: #ccc;
980 | }
981 |
982 | .pure-form input:focus:invalid,
983 | .pure-form textarea:focus:invalid,
984 | .pure-form select:focus:invalid {
985 | color: #b94a48;
986 | border-color: #e9322d;
987 | }
988 | .pure-form input[type="file"]:focus:invalid:focus,
989 | .pure-form input[type="radio"]:focus:invalid:focus,
990 | .pure-form input[type="checkbox"]:focus:invalid:focus {
991 | outline-color: #e9322d;
992 | }
993 | .pure-form select {
994 | /* Normalizes the height; padding is not sufficient. */
995 | height: 2.25em;
996 | border: 1px solid #ccc;
997 | background-color: white;
998 | }
999 | .pure-form select[multiple] {
1000 | height: auto;
1001 | }
1002 | .pure-form label {
1003 | margin: 0.5em 0 0.2em;
1004 | }
1005 | .pure-form fieldset {
1006 | margin: 0;
1007 | padding: 0.35em 0 0.75em;
1008 | border: 0;
1009 | }
1010 | .pure-form legend {
1011 | display: block;
1012 | width: 100%;
1013 | padding: 0.3em 0;
1014 | margin-bottom: 0.3em;
1015 | color: #333;
1016 | border-bottom: 1px solid #e5e5e5;
1017 | }
1018 |
1019 | .pure-form-stacked input[type="text"],
1020 | .pure-form-stacked input[type="password"],
1021 | .pure-form-stacked input[type="email"],
1022 | .pure-form-stacked input[type="url"],
1023 | .pure-form-stacked input[type="date"],
1024 | .pure-form-stacked input[type="month"],
1025 | .pure-form-stacked input[type="time"],
1026 | .pure-form-stacked input[type="datetime"],
1027 | .pure-form-stacked input[type="datetime-local"],
1028 | .pure-form-stacked input[type="week"],
1029 | .pure-form-stacked input[type="number"],
1030 | .pure-form-stacked input[type="search"],
1031 | .pure-form-stacked input[type="tel"],
1032 | .pure-form-stacked input[type="color"],
1033 | .pure-form-stacked input[type="file"],
1034 | .pure-form-stacked select,
1035 | .pure-form-stacked label,
1036 | .pure-form-stacked textarea {
1037 | display: block;
1038 | margin: 0.25em 0;
1039 | }
1040 |
1041 | /*
1042 | Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
1043 | since IE8 won't execute CSS that contains a CSS3 selector.
1044 | */
1045 | .pure-form-stacked input:not([type]) {
1046 | display: block;
1047 | margin: 0.25em 0;
1048 | }
1049 | .pure-form-aligned input,
1050 | .pure-form-aligned textarea,
1051 | .pure-form-aligned select,
1052 | /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */
1053 | .pure-form-aligned .pure-help-inline,
1054 | .pure-form-message-inline {
1055 | display: inline-block;
1056 | *display: inline;
1057 | *zoom: 1;
1058 | vertical-align: middle;
1059 | }
1060 | .pure-form-aligned textarea {
1061 | vertical-align: top;
1062 | }
1063 |
1064 | /* Aligned Forms */
1065 | .pure-form-aligned .pure-control-group {
1066 | margin-bottom: 0.5em;
1067 | }
1068 | .pure-form-aligned .pure-control-group label {
1069 | text-align: right;
1070 | display: inline-block;
1071 | vertical-align: middle;
1072 | width: 10em;
1073 | margin: 0 1em 0 0;
1074 | }
1075 | .pure-form-aligned .pure-controls {
1076 | margin: 1.5em 0 0 11em;
1077 | }
1078 |
1079 | /* Rounded Inputs */
1080 | .pure-form input.pure-input-rounded,
1081 | .pure-form .pure-input-rounded {
1082 | border-radius: 2em;
1083 | padding: 0.5em 1em;
1084 | }
1085 |
1086 | /* Grouped Inputs */
1087 | .pure-form .pure-group fieldset {
1088 | margin-bottom: 10px;
1089 | }
1090 | .pure-form .pure-group input,
1091 | .pure-form .pure-group textarea {
1092 | display: block;
1093 | padding: 10px;
1094 | margin: 0 0 -1px;
1095 | border-radius: 0;
1096 | position: relative;
1097 | top: -1px;
1098 | }
1099 | .pure-form .pure-group input:focus,
1100 | .pure-form .pure-group textarea:focus {
1101 | z-index: 3;
1102 | }
1103 | .pure-form .pure-group input:first-child,
1104 | .pure-form .pure-group textarea:first-child {
1105 | top: 1px;
1106 | border-radius: 4px 4px 0 0;
1107 | margin: 0;
1108 | }
1109 | .pure-form .pure-group input:first-child:last-child,
1110 | .pure-form .pure-group textarea:first-child:last-child {
1111 | top: 1px;
1112 | border-radius: 4px;
1113 | margin: 0;
1114 | }
1115 | .pure-form .pure-group input:last-child,
1116 | .pure-form .pure-group textarea:last-child {
1117 | top: -2px;
1118 | border-radius: 0 0 4px 4px;
1119 | margin: 0;
1120 | }
1121 | .pure-form .pure-group button {
1122 | margin: 0.35em 0;
1123 | }
1124 |
1125 | .pure-form .pure-input-1 {
1126 | width: 100%;
1127 | }
1128 | .pure-form .pure-input-2-3 {
1129 | width: 66%;
1130 | }
1131 | .pure-form .pure-input-1-2 {
1132 | width: 50%;
1133 | }
1134 | .pure-form .pure-input-1-3 {
1135 | width: 33%;
1136 | }
1137 | .pure-form .pure-input-1-4 {
1138 | width: 25%;
1139 | }
1140 |
1141 | /* Inline help for forms */
1142 | /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */
1143 | .pure-form .pure-help-inline,
1144 | .pure-form-message-inline {
1145 | display: inline-block;
1146 | padding-left: 0.3em;
1147 | color: #666;
1148 | vertical-align: middle;
1149 | font-size: 0.875em;
1150 | }
1151 |
1152 | /* Block help for forms */
1153 | .pure-form-message {
1154 | display: block;
1155 | color: #666;
1156 | font-size: 0.875em;
1157 | }
1158 |
1159 | @media only screen and (max-width : 480px) {
1160 | .pure-form button[type="submit"] {
1161 | margin: 0.7em 0 0;
1162 | }
1163 |
1164 | .pure-form input:not([type]),
1165 | .pure-form input[type="text"],
1166 | .pure-form input[type="password"],
1167 | .pure-form input[type="email"],
1168 | .pure-form input[type="url"],
1169 | .pure-form input[type="date"],
1170 | .pure-form input[type="month"],
1171 | .pure-form input[type="time"],
1172 | .pure-form input[type="datetime"],
1173 | .pure-form input[type="datetime-local"],
1174 | .pure-form input[type="week"],
1175 | .pure-form input[type="number"],
1176 | .pure-form input[type="search"],
1177 | .pure-form input[type="tel"],
1178 | .pure-form input[type="color"],
1179 | .pure-form label {
1180 | margin-bottom: 0.3em;
1181 | display: block;
1182 | }
1183 |
1184 | .pure-group input:not([type]),
1185 | .pure-group input[type="text"],
1186 | .pure-group input[type="password"],
1187 | .pure-group input[type="email"],
1188 | .pure-group input[type="url"],
1189 | .pure-group input[type="date"],
1190 | .pure-group input[type="month"],
1191 | .pure-group input[type="time"],
1192 | .pure-group input[type="datetime"],
1193 | .pure-group input[type="datetime-local"],
1194 | .pure-group input[type="week"],
1195 | .pure-group input[type="number"],
1196 | .pure-group input[type="search"],
1197 | .pure-group input[type="tel"],
1198 | .pure-group input[type="color"] {
1199 | margin-bottom: 0;
1200 | }
1201 |
1202 | .pure-form-aligned .pure-control-group label {
1203 | margin-bottom: 0.3em;
1204 | text-align: left;
1205 | display: block;
1206 | width: 100%;
1207 | }
1208 |
1209 | .pure-form-aligned .pure-controls {
1210 | margin: 1.5em 0 0 0;
1211 | }
1212 |
1213 | /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */
1214 | .pure-form .pure-help-inline,
1215 | .pure-form-message-inline,
1216 | .pure-form-message {
1217 | display: block;
1218 | font-size: 0.75em;
1219 | /* Increased bottom padding to make it group with its related input element. */
1220 | padding: 0.2em 0 0.8em;
1221 | }
1222 | }
1223 |
1224 | /*csslint adjoining-classes: false, box-model:false*/
1225 | .pure-menu {
1226 | -webkit-box-sizing: border-box;
1227 | -moz-box-sizing: border-box;
1228 | box-sizing: border-box;
1229 | }
1230 |
1231 | .pure-menu-fixed {
1232 | position: fixed;
1233 | left: 0;
1234 | top: 0;
1235 | z-index: 3;
1236 | }
1237 |
1238 | .pure-menu-list,
1239 | .pure-menu-item {
1240 | position: relative;
1241 | }
1242 |
1243 | .pure-menu-list {
1244 | list-style: none;
1245 | margin: 0;
1246 | padding: 0;
1247 | }
1248 |
1249 | .pure-menu-item {
1250 | padding: 0;
1251 | margin: 0;
1252 | height: 100%;
1253 | }
1254 |
1255 | .pure-menu-link,
1256 | .pure-menu-heading {
1257 | display: block;
1258 | text-decoration: none;
1259 | white-space: nowrap;
1260 | }
1261 |
1262 | /* HORIZONTAL MENU */
1263 | .pure-menu-horizontal {
1264 | width: 100%;
1265 | white-space: nowrap;
1266 | }
1267 |
1268 | .pure-menu-horizontal .pure-menu-list {
1269 | display: inline-block;
1270 | }
1271 |
1272 | /* Initial menus should be inline-block so that they are horizontal */
1273 | .pure-menu-horizontal .pure-menu-item,
1274 | .pure-menu-horizontal .pure-menu-heading,
1275 | .pure-menu-horizontal .pure-menu-separator {
1276 | display: inline-block;
1277 | *display: inline;
1278 | zoom: 1;
1279 | vertical-align: middle;
1280 | }
1281 |
1282 | /* Submenus should still be display: block; */
1283 | .pure-menu-item .pure-menu-item {
1284 | display: block;
1285 | }
1286 |
1287 | .pure-menu-children {
1288 | display: none;
1289 | position: absolute;
1290 | left: 100%;
1291 | top: 0;
1292 | margin: 0;
1293 | padding: 0;
1294 | z-index: 3;
1295 | }
1296 |
1297 | .pure-menu-horizontal .pure-menu-children {
1298 | left: 0;
1299 | top: auto;
1300 | width: inherit;
1301 | }
1302 |
1303 | .pure-menu-allow-hover:hover > .pure-menu-children,
1304 | .pure-menu-active > .pure-menu-children {
1305 | display: block;
1306 | position: absolute;
1307 | }
1308 |
1309 | /* Vertical Menus - show the dropdown arrow */
1310 | .pure-menu-has-children > .pure-menu-link:after {
1311 | padding-left: 0.5em;
1312 | content: "\25B8";
1313 | font-size: small;
1314 | }
1315 |
1316 | /* Horizontal Menus - show the dropdown arrow */
1317 | .pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after {
1318 | content: "\25BE";
1319 | }
1320 |
1321 | /* scrollable menus */
1322 | .pure-menu-scrollable {
1323 | overflow-y: scroll;
1324 | overflow-x: hidden;
1325 | }
1326 |
1327 | .pure-menu-scrollable .pure-menu-list {
1328 | display: block;
1329 | }
1330 |
1331 | .pure-menu-horizontal.pure-menu-scrollable .pure-menu-list {
1332 | display: inline-block;
1333 | }
1334 |
1335 | .pure-menu-horizontal.pure-menu-scrollable {
1336 | white-space: nowrap;
1337 | overflow-y: hidden;
1338 | overflow-x: auto;
1339 | -ms-overflow-style: none;
1340 | -webkit-overflow-scrolling: touch;
1341 | /* a little extra padding for this style to allow for scrollbars */
1342 | padding: .5em 0;
1343 | }
1344 |
1345 | .pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar {
1346 | display: none;
1347 | }
1348 |
1349 | /* misc default styling */
1350 |
1351 | .pure-menu-separator {
1352 | background-color: #ccc;
1353 | height: 1px;
1354 | margin: .3em 0;
1355 | }
1356 |
1357 | .pure-menu-horizontal .pure-menu-separator {
1358 | width: 1px;
1359 | height: 1.3em;
1360 | margin: 0 .3em ;
1361 | }
1362 |
1363 | .pure-menu-heading {
1364 | text-transform: uppercase;
1365 | color: #565d64;
1366 | }
1367 |
1368 | .pure-menu-link {
1369 | color: #777;
1370 | }
1371 |
1372 | .pure-menu-children {
1373 | background-color: #fff;
1374 | }
1375 |
1376 | .pure-menu-link,
1377 | .pure-menu-disabled,
1378 | .pure-menu-heading {
1379 | padding: .5em 1em;
1380 | }
1381 |
1382 | .pure-menu-disabled {
1383 | opacity: .5;
1384 | }
1385 |
1386 | .pure-menu-disabled .pure-menu-link:hover {
1387 | background-color: transparent;
1388 | }
1389 |
1390 | .pure-menu-active > .pure-menu-link,
1391 | .pure-menu-link:hover,
1392 | .pure-menu-link:focus {
1393 | background-color: #eee;
1394 | }
1395 |
1396 | .pure-menu-selected .pure-menu-link,
1397 | .pure-menu-selected .pure-menu-link:visited {
1398 | color: #000;
1399 | }
1400 |
1401 | .pure-table {
1402 | /* Remove spacing between table cells (from Normalize.css) */
1403 | border-collapse: collapse;
1404 | border-spacing: 0;
1405 | empty-cells: show;
1406 | border: 1px solid #cbcbcb;
1407 | }
1408 |
1409 | .pure-table caption {
1410 | color: #000;
1411 | font: italic 85%/1 arial, sans-serif;
1412 | padding: 1em 0;
1413 | text-align: center;
1414 | }
1415 |
1416 | .pure-table td,
1417 | .pure-table th {
1418 | border-left: 1px solid #cbcbcb;/* inner column border */
1419 | border-width: 0 0 0 1px;
1420 | font-size: inherit;
1421 | margin: 0;
1422 | overflow: visible; /*to make ths where the title is really long work*/
1423 | padding: 0.5em 1em; /* cell padding */
1424 | }
1425 |
1426 | /* Consider removing this next declaration block, as it causes problems when
1427 | there's a rowspan on the first cell. Case added to the tests. issue#432 */
1428 | .pure-table td:first-child,
1429 | .pure-table th:first-child {
1430 | border-left-width: 0;
1431 | }
1432 |
1433 | .pure-table thead {
1434 | background-color: #e0e0e0;
1435 | color: #000;
1436 | text-align: left;
1437 | vertical-align: bottom;
1438 | }
1439 |
1440 | /*
1441 | striping:
1442 | even - #fff (white)
1443 | odd - #f2f2f2 (light gray)
1444 | */
1445 | .pure-table td {
1446 | background-color: transparent;
1447 | }
1448 | .pure-table-odd td {
1449 | background-color: #f2f2f2;
1450 | }
1451 |
1452 | /* nth-child selector for modern browsers */
1453 | .pure-table-striped tr:nth-child(2n-1) td {
1454 | background-color: #f2f2f2;
1455 | }
1456 |
1457 | /* BORDERED TABLES */
1458 | .pure-table-bordered td {
1459 | border-bottom: 1px solid #cbcbcb;
1460 | }
1461 | .pure-table-bordered tbody > tr:last-child > td {
1462 | border-bottom-width: 0;
1463 | }
1464 |
1465 |
1466 | /* HORIZONTAL BORDERED TABLES */
1467 |
1468 | .pure-table-horizontal td,
1469 | .pure-table-horizontal th {
1470 | border-width: 0 0 1px 0;
1471 | border-bottom: 1px solid #cbcbcb;
1472 | }
1473 | .pure-table-horizontal tbody > tr:last-child > td {
1474 | border-bottom-width: 0;
1475 | }
1476 |
--------------------------------------------------------------------------------
/style/spinner.styl:
--------------------------------------------------------------------------------
1 | /* :not(:required) hides this rule from IE9 and below */
2 | .Pulse-async-spinner:not(:required)
3 | animation: Pulse-async-spinner 1250ms infinite linear
4 | border: 5px solid #1B98F8
5 | border-right-color: transparent
6 | border-radius: 12px
7 | box-sizing: border-box
8 | display: inline-block
9 | position: relative
10 | overflow: hidden
11 | text-indent: -9999px
12 | width: 24px
13 | height: 24px
14 |
15 | @keyframes Pulse-async-spinner
16 | 0%
17 | transform: rotate(0deg)
18 | 100%
19 | transform: rotate(360deg)
--------------------------------------------------------------------------------
/test/actions.test.js:
--------------------------------------------------------------------------------
1 | /* global describe */
2 | /* global it */
3 | /* global afterEach */
4 |
5 | import sinon from 'sinon';
6 | import chai from 'chai';
7 |
8 | var expect = chai.expect;
9 |
10 | import configureStore from 'redux-mock-store';
11 | import thunk from 'redux-thunk';
12 |
13 | import { setUserId, loadEvents, __RewireAPI__ as actions } from '../universal/actions/PulseActions.js';
14 | import * as types from '../universal/constants/ActionTypes';
15 |
16 | describe('Actions', () => {
17 | afterEach(function() {
18 | actions.__ResetDependency__('request');
19 | });
20 |
21 | /**
22 | * Example of writing a test on a syncronous action creator
23 | */
24 | describe('setUserId', () => {
25 | it('should return action with type SET_USER_ID and userId equal to 200', () => {
26 | let action = setUserId(200);
27 | expect(action.type).to.equal(types.SET_USER_ID);
28 | expect(action.userId).to.equal(200);
29 | });
30 |
31 | it('should return action with type SET_USER_ID and userId equal to 6700102', () => {
32 | let action = setUserId(6700102);
33 | expect(action.type).to.equal(types.SET_USER_ID);
34 | expect(action.userId).to.equal(6700102);
35 | });
36 | });
37 |
38 | /**
39 | * Example of writing a test on an asyncronous action creator
40 | */
41 | describe('loadEvents', () => {
42 | const mockStore = configureStore([thunk]);
43 | it('should trigger a LOAD_EVENTS_REQUEST and LOAD_EVENTS_SUCCESS action when succesful', () => {
44 | let requestMock = {
45 | get: () => ({
46 | set: () => ({
47 | end: (x) => x(null, {
48 | body: [ { name: 'Awesome', value: 54 } ]
49 | })
50 | })
51 | })
52 | };
53 |
54 | actions.__Rewire__('request', requestMock);
55 |
56 | let expectedActions = [
57 | { type: 'LOAD_EVENTS_REQUEST' },
58 | { type: 'LOAD_EVENTS_SUCCESS', events: [ { name: 'Awesome', value: 54 } ] }
59 | ];
60 |
61 | let initialState = {pulseApp: { events: [], userId: 'baseUser'} };
62 | let store = mockStore(initialState);
63 |
64 | store.dispatch(loadEvents());
65 |
66 | const actualActions = store.getActions();
67 |
68 | expect(actualActions).to.eql(expectedActions);
69 | });
70 |
71 | it('should trigger a LOAD_EVENTS_REQUEST and LOAD_EVENTS_FAILURE action when unsuccessful', () => {
72 | let error = 'An Error Occurred!';
73 | let requestMock = {
74 | get: () => ({
75 | set: () => ({
76 | end: (x) => x(error)
77 | })
78 | })
79 | };
80 |
81 | actions.__Rewire__('request', requestMock);
82 |
83 | let expectedActions = [
84 | { type: 'LOAD_EVENTS_REQUEST' },
85 | { type: 'LOAD_EVENTS_FAILURE', error: error }
86 | ];
87 |
88 | let initialState = {pulseApp: { events: [], userId: 'baseUser'} };
89 | let store = mockStore(initialState);
90 |
91 | store.dispatch(loadEvents());
92 |
93 | const actualActions = store.getActions();
94 |
95 | expect(actualActions).to.eql(expectedActions);
96 | });
97 | });
98 | });
--------------------------------------------------------------------------------
/test/reducers.test.js:
--------------------------------------------------------------------------------
1 | /* global describe */
2 | /* global it */
3 | /* global afterEach */
4 |
5 | import 'babel-polyfill'; // For use of Object.assign
6 |
7 | import sinon from 'sinon';
8 | import chai from 'chai';
9 |
10 | var expect = chai.expect;
11 |
12 | import reducer from '../universal/reducers';
13 | import * as actions from '../universal/actions/PulseActions.js';
14 |
15 | describe('Reducers', () => {
16 |
17 | /**
18 | * Example of writing a test on a reducing functiom
19 | */
20 | describe('setUserId', () => {
21 | it('should set user id', () => {
22 | let initialStateForTest = { userId: null };
23 | let userId = 234;
24 | let action = actions.setUserId(userId);
25 |
26 | expect(initialStateForTest.userId).to.be.null;
27 |
28 | let state = reducer(initialStateForTest, action);
29 | expect(state.userId).to.equal(userId);
30 | });
31 | });
32 |
33 | describe('addEvent', () => {
34 | describe('request', () => {
35 | it('should set isWorking to true', () => {
36 | let initialStateForTest = { isWorking: false };
37 | let action = actions.addEventRequest();
38 |
39 | expect(initialStateForTest.isWorking).to.be.false;
40 |
41 | let state = reducer(initialStateForTest, action);
42 | expect(state.isWorking).to.be.true;
43 | });
44 | });
45 |
46 | describe('success', () => {
47 | it('should set isWorking to false and add event to events', () => {
48 | let events = [
49 | { id: 22, name: 'Entry', value: 20 }
50 | ];
51 | let initialStateForTest = { isWorking: true, events: events };
52 | let event = { id: 25, name: 'Another Entry', value: 50 };
53 |
54 | let action = actions.addEventSuccess(event);
55 |
56 | expect(initialStateForTest.isWorking).to.be.true;
57 | expect(initialStateForTest.events.length).to.equal(events.length);
58 |
59 |
60 | let state = reducer(initialStateForTest, action);
61 | expect(state.isWorking).to.be.false;
62 | expect(state.events.length).to.equal(events.length + 1);
63 | });
64 | });
65 |
66 | describe('failure', () => {
67 | it('should set isWorking to false and error and not change events', () => {
68 | let events = [
69 | { id: 22, name: 'Entry', value: 20 }
70 | ];
71 | let initialStateForTest = { isWorking: true, events: events, error: null };
72 | let error = 'some error';
73 |
74 | let action = actions.addEventFailure(error);
75 |
76 | expect(initialStateForTest.isWorking).to.be.true;
77 | expect(initialStateForTest.error).to.be.null;
78 | expect(initialStateForTest.events.length).to.equal(events.length);
79 |
80 |
81 | let state = reducer(initialStateForTest, action);
82 | expect(state.isWorking).to.be.false;
83 | expect(state.error).to.equal(error);
84 | expect(state.events.length).to.equal(events.length);
85 | });
86 | });
87 | });
88 | });
--------------------------------------------------------------------------------
/tests.webpack.js:
--------------------------------------------------------------------------------
1 | var context = require.context('./test', true, /.js$/); // Load files in /test with filename matching * .test.js
2 | context.keys().forEach(context);
--------------------------------------------------------------------------------
/universal/actions/PulseActions.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes';
2 | import request from 'superagent';
3 |
4 | const serverUrl = '';
5 | const eventsUrl = `${serverUrl}/api/0/events`;
6 |
7 | export function setUserId(userId) {
8 | return {
9 | type: types.SET_USER_ID,
10 | userId
11 | };
12 | }
13 |
14 | export function loadEvents() {
15 | return dispatch => {
16 | dispatch(loadEventsRequest());
17 | return request
18 | .get(eventsUrl)
19 | .set('Accept', 'application/json')
20 | .end((err, res) => {
21 | if (err) {
22 | dispatch(loadEventsFailure(err));
23 | } else {
24 | dispatch(loadEventsSuccess(res.body));
25 | }
26 | });
27 | };
28 | }
29 |
30 | export function loadEventsRequest() {
31 | return {
32 | type: types.LOAD_EVENTS_REQUEST
33 | };
34 | }
35 |
36 | export function loadEventsSuccess(events) {
37 | return {
38 | type: types.LOAD_EVENTS_SUCCESS,
39 | events
40 | };
41 | }
42 |
43 | export function loadEventsFailure(error) {
44 | return {
45 | type: types.LOAD_EVENTS_FAILURE,
46 | error
47 | };
48 | }
49 |
50 | export function addEvent(event) {
51 | console.log('Add event', event);
52 | return dispatch => {
53 | dispatch(addEventRequest(event));
54 |
55 | return request
56 | .post(eventsUrl)
57 | .send(event)
58 | .set('Accept', 'application/json')
59 | .end((err, res) => {
60 | if (err) {
61 | dispatch(addEventFailure(err, event));
62 | } else {
63 | dispatch(addEventSuccess(res.body));
64 | }
65 | });
66 | };
67 | }
68 |
69 | export function addEventRequest(event) {
70 | return {
71 | type: types.ADD_EVENT_REQUEST,
72 | event
73 | };
74 | }
75 |
76 | export function addEventSuccess(event) {
77 | return {
78 | type: types.ADD_EVENT_SUCCESS,
79 | event
80 | };
81 | }
82 |
83 | export function addEventFailure(error, event) {
84 | return {
85 | type: types.ADD_EVENT_FAILURE,
86 | error
87 | };
88 | }
89 |
90 | export function deleteEvent(event) {
91 | return dispatch => {
92 | dispatch(deleteEventRequest(event));
93 |
94 | return request
95 | .del(eventsUrl + '/' + event.id)
96 | .set('Accept', 'application/json')
97 | .end((err, res) => {
98 | if (err) {
99 | dispatch(deleteEventFailure(err, event));
100 | } else {
101 | dispatch(deleteEventSuccess(res.body));
102 | }
103 | });
104 | };
105 | }
106 |
107 | export function deleteEventRequest(event) {
108 | return {
109 | type: types.DELETE_EVENT_REQUEST,
110 | event
111 | };
112 | }
113 |
114 | export function deleteEventSuccess(event) {
115 | return {
116 | type: types.DELETE_EVENT_SUCCESS,
117 | event
118 | };
119 | }
120 |
121 | export function deleteEventFailure(error, event) {
122 | return {
123 | type: types.DELETE_EVENT_FAILURE,
124 | error,
125 | event
126 | };
127 | }
128 |
129 | export function editEvent(event) {
130 | return dispatch => {
131 | dispatch(editEventRequest(event));
132 |
133 | return request
134 | .post(eventsUrl + '/' + event.id)
135 | .send(event)
136 | .set('Accept', 'application/json')
137 | .end((err, res) => {
138 | if (err) {
139 | dispatch(editEventFailure(err, event));
140 | } else {
141 | dispatch(editEventSuccess(res.body));
142 | }
143 | });
144 | };
145 | }
146 |
147 | export function editEventRequest(event) {
148 | return {
149 | type: types.EDIT_EVENT_REQUEST,
150 | event
151 | };
152 | }
153 |
154 | export function editEventSuccess(event) {
155 | return {
156 | type: types.EDIT_EVENT_SUCCESS,
157 | event
158 | };
159 | }
160 |
161 | export function editEventFailure(error, event) {
162 | return {
163 | type: types.EDIT_EVENT_FAILURE,
164 | error,
165 | event
166 | };
167 | }
--------------------------------------------------------------------------------
/universal/components/AsyncBar.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 |
3 | export default class AsyncBar extends Component {
4 | static propTypes = {
5 | isWorking: PropTypes.bool,
6 | error: PropTypes.string
7 | };
8 |
9 | render() {
10 | let spinner = (this.props.isWorking) ? this.renderSpinner() : null;
11 | let error = (this.props.error) ? this.renderError() : null;
12 |
13 | return (
14 |
15 | {spinner}
16 | {error}
17 |
18 | );
19 | }
20 |
21 | renderSpinner() {
22 | return (
23 |
24 | Loading…
25 |
26 | );
27 | }
28 |
29 | renderError() {
30 | return (
31 |
32 | {this.props.error}
33 |
34 | );
35 | }
36 | }
--------------------------------------------------------------------------------
/universal/components/EventInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { VALUE_CLASSES } from '../constants/ActionTypes.js';
3 |
4 | export default class EventInput extends Component {
5 | static propTypes = {
6 | onSubmit: PropTypes.func.isRequired,
7 | userId: PropTypes.string.isRequired,
8 | textLabel: PropTypes.string,
9 | valueLabel: PropTypes.string,
10 | editing: PropTypes.bool
11 | };
12 |
13 | constructor(props, context) {
14 | super(props, context);
15 | this.state = {
16 | errors: [],
17 | text: this.props.text || '',
18 | value: this.props.value || 50
19 | };
20 | }
21 |
22 | handleSubmit(e) {
23 | let errors;
24 | e.preventDefault();
25 |
26 | if (this.state.text.length === 0) {
27 | errors = ['You have not said what happened!'];
28 | }
29 |
30 | if (this.state.value < 1 || this.state.value > 100) {
31 | errors = [...errors, 'You have somewhere set an invalid value!'];
32 | }
33 |
34 | if (errors && errors.length > 0) {
35 | this.setState({errors: errors});
36 | } else {
37 | this.props.onSubmit({text: this.state.text, value: this.state.value, userId: this.props.userId});
38 | this.setState({text: '', value: 50});
39 | }
40 | }
41 |
42 | handleTextChange(e) {
43 | this.setState({ text: e.target.value });
44 | }
45 |
46 | handleValueChange(e) {
47 | this.setState({ value: parseInt(e.target.value, 10) });
48 | }
49 |
50 | render() {
51 | let self = this;
52 | let saveText = (this.props.editing) ? 'Save' : 'Add';
53 | let className = Object.keys(VALUE_CLASSES).reduce((current, key) => {
54 | if (!current && self.state.value <= key) {
55 | return VALUE_CLASSES[key];
56 | } else {
57 | return current;
58 | }
59 | }, null);
60 |
61 | return (
62 |
71 | );
72 | }
73 | }
--------------------------------------------------------------------------------
/universal/components/EventItem.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import distanceInWords from 'date-fns/distance_in_words';
3 | import EventInput from './EventInput';
4 |
5 | export default class EventItem extends Component {
6 | static propTypes = {
7 | id: PropTypes.any.isRequired,
8 | row: PropTypes.number.isRequired,
9 | event: PropTypes.object.isRequired,
10 | editable: PropTypes.bool,
11 | editEvent: PropTypes.func,
12 | deleteEvent: PropTypes.func
13 | };
14 |
15 | constructor(props, context){
16 | super(props, context);
17 | this.state = {
18 | editing: false
19 | };
20 | }
21 |
22 | handleClick() {
23 | if (this.props.editable) {
24 | this.setState({ editing: true });
25 | }
26 | }
27 |
28 | handleSave(event) {
29 | if (event.text.length === 0) {
30 | this.props.deleteEvent(event);
31 | } else {
32 | this.props.editEvent(event);
33 | }
34 | this.setState({ editing: false });
35 | }
36 |
37 | render() {
38 | const { row, id, event, editEvent, deleteEvent } = this.props;
39 |
40 | let element, className = (row % 2 === 0) ? 'even' : 'odd';
41 | let modified = (event.updated) ? event.updated : event.created;
42 |
43 | if (this.state.editing) {
44 | element = (
45 | this.handleSave(Object.assign({}, event, { id: id })) } />
51 | );
52 | } else {
53 | let del = (this.props.editable) ?
54 |