├── backups
└── .gitkeep
├── nginx
├── ssl
│ └── .gitkeep
├── Dockerfile
├── setupconf.sh
├── nginx_nossl.conf
└── nginx_ssl.conf
├── frontend
├── dist
│ └── .gitkeep
├── universal
│ ├── redux
│ │ ├── reducers
│ │ │ └── index.js
│ │ ├── createStore.js
│ │ ├── createStore.prod.js
│ │ └── createStore.dev.js
│ ├── modules
│ │ ├── counter
│ │ │ ├── components
│ │ │ │ ├── CountersList
│ │ │ │ │ ├── counter-list.css
│ │ │ │ │ └── CountersList.js
│ │ │ │ ├── CountersPage
│ │ │ │ │ ├── counters-page.css
│ │ │ │ │ └── CountersPage.js
│ │ │ │ ├── CounterListItem
│ │ │ │ │ ├── counter-list-item.css
│ │ │ │ │ └── CounterListItem.js
│ │ │ │ └── Counter
│ │ │ │ │ ├── Counter.css
│ │ │ │ │ └── Counter.js
│ │ │ ├── containers
│ │ │ │ ├── Counters
│ │ │ │ │ └── CountersContainer.js
│ │ │ │ └── Counter
│ │ │ │ │ └── CounterContainer.js
│ │ │ └── ducks
│ │ │ │ └── counter.js
│ │ ├── location
│ │ │ └── ducks
│ │ │ │ └── location.js
│ │ └── root.js
│ ├── styles
│ │ ├── reset.css
│ │ ├── colors.css
│ │ ├── typography.css
│ │ └── button.css
│ ├── routes
│ │ ├── static.js
│ │ ├── Routes.js
│ │ └── async.js
│ ├── components
│ │ ├── Home
│ │ │ ├── Home.css
│ │ │ └── Home.js
│ │ └── App
│ │ │ ├── App.js
│ │ │ └── App.css
│ ├── containers
│ │ └── App
│ │ │ └── AppContainer.js
│ └── graphql
│ │ ├── base.js
│ │ └── counter.js
├── .babelrc
├── webpack
│ ├── dev-server.js
│ ├── webpack.config.development.js
│ ├── webpack.config.server.js
│ └── webpack.config.production.js
├── .eslintrc
├── index.js
└── package.json
├── backend
├── .babelrc
├── config
│ ├── development.json
│ ├── test.json
│ ├── production.json
│ └── default.json
├── .dockerignore
├── lib
│ ├── api
│ │ ├── index.js
│ │ └── notes.js
│ ├── index.js
│ ├── models
│ │ ├── Counter.js
│ │ └── index.js
│ ├── migrations
│ │ └── 20160425105021-create-counter.js
│ ├── conf.js
│ ├── logger.js
│ ├── app.js
│ ├── ssr.js
│ ├── graphql
│ │ └── index.js
│ └── Html.js
├── Dockerfile
├── tests
│ ├── specs
│ │ └── notes.js
│ └── globals.js
├── .sequelizerc
├── nightwatch.json
└── package.json
├── .env
├── bin
├── sequelize.sh
├── sequelize_prod.sh
├── start_production.sh
├── stop_production.sh
├── npm_backend.sh
├── npm_frontend.sh
├── psql.sh
├── build_frontend.sh
├── build_production.sh
├── backup.sh
├── env.sh
├── develop.sh
├── test.sh
├── deploy.sh
├── restore.sh
└── init_db.sh
├── docker-compose.db.yml
├── docker-compose.yml
├── docker-compose.test.yml
├── docker-compose.development.yml
├── docker-compose.production.yml
├── .gitignore
├── LICENSE
└── README.md
/backups/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nginx/ssl/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/dist/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "react", "stage-0"]
3 | }
4 |
--------------------------------------------------------------------------------
/backend/config/development.json:
--------------------------------------------------------------------------------
1 | {
2 | "webpack_dev_server": true
3 | }
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | media/*
3 | tests/*
4 | nightwatch.js
5 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | NODE_ENV_PROD=production
3 | POSTGRES_USER=appuser
4 | POSTGRES_DB=appdb
--------------------------------------------------------------------------------
/bin/sequelize.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | source bin/env.sh
4 |
5 | dcdev run --rm backend ./node_modules/.bin/sequelize $@
--------------------------------------------------------------------------------
/frontend/universal/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | export {default as counter} from '../../modules/counter/ducks/counter.js';
2 |
--------------------------------------------------------------------------------
/bin/sequelize_prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | source bin/env.sh
4 |
5 | dcprod run --rm backend ./node_modules/.bin/sequelize $@
--------------------------------------------------------------------------------
/bin/start_production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # start production server
3 |
4 | source bin/env.sh
5 | dcprod up -d
6 | echo "started"
--------------------------------------------------------------------------------
/bin/stop_production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # stop production server
3 |
4 | source bin/env.sh
5 | dcprod stop
6 | echo "stopped"
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/CountersList/counter-list.css:
--------------------------------------------------------------------------------
1 | .list {
2 | padding: 0;
3 | margin: 0 auto;
4 | width: 70%;
5 | }
6 |
--------------------------------------------------------------------------------
/bin/npm_backend.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #run npm command. use this to install new packages to dev
4 | source bin/env.sh
5 |
6 | dcdev run --rm backend npm $@
--------------------------------------------------------------------------------
/frontend/universal/modules/location/ducks/location.js:
--------------------------------------------------------------------------------
1 | export const locationCurrentCounterId = (state, ownProps) => ( ownProps.match.params.counterId )
2 |
--------------------------------------------------------------------------------
/bin/npm_frontend.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #run npm command. use this to install new packages to dev
4 | source bin/env.sh
5 |
6 | dcdev run --rm frontend npm $@
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/CountersPage/counters-page.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 70%;
3 | margin: 0 auto;
4 | padding-bottom: 3rem;
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/universal/styles/reset.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-family: var(--font-family);
3 | font-size: 10px;
4 | }
5 |
6 | h1 {
7 | color: var(--midnight-blue);
8 | }
9 |
--------------------------------------------------------------------------------
/backend/lib/api/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import notes from './notes';
3 |
4 | const router = express.Router();
5 | router.use(notes);
6 | export default router;
--------------------------------------------------------------------------------
/bin/psql.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # open psql session to production db
3 |
4 | source bin/env.sh
5 |
6 | dcprod -f docker-compose.db.yml run --rm dbclient bash -c 'psql -h db -U $POSTGRES_USER $POSTGRES_DB'
7 |
--------------------------------------------------------------------------------
/frontend/universal/redux/createStore.js:
--------------------------------------------------------------------------------
1 |
2 | if (process.env.NODE_ENV === 'production') {
3 | module.exports = require('./createStore.prod');
4 | } else {
5 | module.exports = require('./createStore.dev');
6 | }
7 |
--------------------------------------------------------------------------------
/bin/build_frontend.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #compile frontend production build to frontend/dist
4 |
5 | source bin/env.sh
6 |
7 | echo "building frontend"
8 | dcdev build
9 | ./bin/npm_frontend.sh i -q
10 | ./bin/npm_frontend.sh run build-prod
--------------------------------------------------------------------------------
/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.9
2 |
3 | ADD ./nginx/ssl /etc/nginx/ssl
4 |
5 | ADD ./nginx/nginx_nossl.conf /etc/nginx/
6 | ADD ./nginx/nginx_ssl.conf /etc/nginx/
7 | ADD ./frontend/dist /static
8 | ADD ./nginx/setupconf.sh .
9 | RUN ./setupconf.sh
--------------------------------------------------------------------------------
/frontend/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "react", "es2015", "stage-0"],
3 | "plugins": [
4 | ["transform-decorators-legacy"],
5 | ["add-module-exports"],
6 | ],
7 | "env": {
8 | "development": {
9 | "presets": ["react-hmre"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/backend/config/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "serve_static_files": true,
3 |
4 | "database": {
5 | "username": "testuser",
6 | "database": "testdb",
7 | "password": null,
8 | "host": "dbtest",
9 | "port": 5432,
10 | "dialect": "postgres"
11 | }
12 | }
--------------------------------------------------------------------------------
/frontend/universal/routes/static.js:
--------------------------------------------------------------------------------
1 | export { default as Home } from '../components/Home/Home.js';
2 | export { default as Counter } from '../modules/counter/containers/Counter/CounterContainer.js';
3 | export { default as Counters } from '../modules/counter/containers/Counters/CountersContainer.js';
4 |
--------------------------------------------------------------------------------
/frontend/universal/styles/colors.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --midnight-blue: #354263;
3 | --bone-white: #fffff8;
4 | --dust-white: #f5f5f5;
5 | --gray-white: #f1f1f1;
6 |
7 |
8 | --primary-color: var(--midnight-blue);
9 | --secondary-color: var(--bone-white);
10 |
11 | --font-family: system-ui;
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/universal/components/Home/Home.css:
--------------------------------------------------------------------------------
1 | .home {
2 | max-width: 750px;
3 | }
4 |
5 | .center {
6 | text-align: center;
7 | }
8 |
9 | .title {
10 | display: block;
11 | text-align: center;
12 | width: 100%;
13 | }
14 |
15 | .button {
16 | display: inline-block;
17 | margin-top: 50px;
18 | }
19 |
--------------------------------------------------------------------------------
/bin/build_production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #builds production images
4 | source bin/env.sh
5 |
6 | echo "building frontend"
7 | ./bin/build_frontend.sh
8 |
9 | echo "copying dependent build files into backend"
10 | cp -R ./frontend/dist/ ./backend/dist/static/
11 |
12 | echo "building backend"
13 | dcprod build
14 |
--------------------------------------------------------------------------------
/docker-compose.db.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | dbclient:
4 | image: postgres:9.6
5 | depends_on:
6 | - db
7 | environment:
8 | POSTGRES_DB: ${POSTGRES_DB}
9 | POSTGRES_USER: ${POSTGRES_USER}
10 | volumes:
11 | - "./backups:/backups"
--------------------------------------------------------------------------------
/bin/backup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # create a db backups to backups/
3 |
4 | source bin/env.sh
5 |
6 | BACKUP_FILE=django$1_$(date +'%Y_%m_%dT%H_%M_%S').bak
7 | dcprod -f docker-compose.db.yml run --rm dbclient bash -c 'pg_dump -Fc -h db -U $POSTGRES_USER -d $POSTGRES_DB -f /backups/'"$BACKUP_FILE"
8 | echo "backup $BACKUP_FILE created"
--------------------------------------------------------------------------------
/backend/lib/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import logger from './logger';
3 | import conf from './conf';
4 | import app from './app';
5 |
6 | const PORT = conf.get('port');
7 | const HOST = conf.get('host');
8 |
9 | app.listen(PORT, HOST, function() {
10 | logger.info(`app started on ${HOST}:${PORT}`);
11 | });
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | dbdata:
4 | image: postgres:9.6
5 | command: "true"
6 |
7 | db:
8 | image: postgres:9.6
9 | environment:
10 | POSTGRES_DB: ${POSTGRES_DB}
11 | POSTGRES_USER: ${POSTGRES_USER}
12 | volumes_from:
13 | - dbdata
14 |
--------------------------------------------------------------------------------
/bin/env.sh:
--------------------------------------------------------------------------------
1 | export DOCKER_CONFIG_PROD=${DOCKER_CONFIG_PROD:-docker-compose.production.yml}
2 | export DOCKER_CONFIG_DEV=${DOCKER_CONFIG_DEV:-docker-compose.development.yml}
3 |
4 |
5 | dcdev() {
6 | docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_DEV "$@"
7 | }
8 |
9 | dcprod() {
10 | docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_PROD "$@"
11 | }
--------------------------------------------------------------------------------
/bin/develop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #start development server on :8000
4 |
5 | source bin/env.sh
6 |
7 | dcdev build
8 | ./bin/init_db.sh
9 | echo "installing backend deps"
10 | ./bin/npm_backend.sh i -q
11 | echo "installing frontend deps"
12 | ./bin/npm_frontend.sh i -q
13 | echo "running migrations"
14 | ./bin/sequelize.sh db:migrate
15 | echo "starting"
16 | dcdev up
17 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | # production dockerfile
2 | FROM node:7.6.0
3 |
4 | # add code
5 | COPY . /backend
6 |
7 | WORKDIR /backend
8 |
9 | #install deps, build, remove initial sources, dev deps
10 | RUN npm i -q && \
11 | npm run compile && \
12 | npm prune -q --production
13 |
14 | #migrate & run
15 | CMD ./node_modules/.bin/sequelize db:migrate && \
16 | npm run serve
17 |
--------------------------------------------------------------------------------
/frontend/universal/styles/typography.css:
--------------------------------------------------------------------------------
1 | .title {
2 | color: var(--midnight-blue);
3 | line-height: 1.5em;
4 | font-size: 3rem;
5 | }
6 |
7 | .link {
8 | color: var(--midnight-blue);
9 | text-decoration: none;
10 | font-family: var(--font-family);
11 | border-bottom: solid 2px transparent;
12 | }
13 |
14 | .link:hover {
15 | border-bottom: solid 2px var(--midnight-blue);
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/universal/containers/App/AppContainer.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import App from 'universal/components/App/App';
3 |
4 | class AppContainer extends Component {
5 | static propTypes = {
6 | children: PropTypes.element.isRequired
7 | };
8 |
9 | render () {
10 | return (
11 |
12 | );
13 | }
14 | }
15 |
16 | export default AppContainer;
17 |
--------------------------------------------------------------------------------
/backend/lib/models/Counter.js:
--------------------------------------------------------------------------------
1 | export default function(sequelize, DataTypes) {
2 | const Counter = sequelize.define('Counter', {
3 | id: {
4 | type: DataTypes.UUID,
5 | primaryKey: true,
6 | },
7 | value: {
8 | type: DataTypes.INTEGER,
9 | allowNull: false
10 | }
11 | }, {
12 | timestamps: true,
13 | });
14 |
15 | return Counter;
16 | };
17 |
--------------------------------------------------------------------------------
/frontend/universal/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 |
3 | import styles from './App.css';
4 |
5 | class App extends Component {
6 | static propTypes = {
7 | children: PropTypes.element.isRequired
8 | };
9 |
10 | render () {
11 | return (
12 |
13 | {this.props.children}
14 |
15 | );
16 | }
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/backend/tests/specs/notes.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'User can navigate to notes': function (browser) {
3 | browser
4 |
5 | .url(browser.launch_url)
6 | .waitForElementPresent('.home')
7 | .assert.containsText('.home h1', 'Example app')
8 |
9 | .click('a[href="/notes"]')
10 | .waitForElementPresent('.notes')
11 | .assert.urlEquals(browser.launch_url+'/notes')
12 |
13 | .end();
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/nginx/setupconf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd /etc/nginx
3 | rm nginx.conf
4 | if ls ssl/*.crt &> /dev/null; then
5 | mv nginx_ssl.conf nginx.conf
6 | CRT=$(ls ssl/*.crt | head -n 1)
7 | KEY=$(ls ssl/*.key | head -n 1)
8 | echo "using ssl, key=${KEY} crt=${CRT}"
9 | sed -i "s#ssl/server.crt#$CRT#g" /etc/nginx/nginx.conf
10 | sed -i "s#ssl/server.key#$KEY#g" /etc/nginx/nginx.conf
11 | else
12 | echo "not using ssl"
13 | mv /etc/nginx/nginx_nossl.conf /etc/nginx/nginx.conf
14 | fi
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/CounterListItem/counter-list-item.css:
--------------------------------------------------------------------------------
1 | .listItem {
2 | min-width: 70%;
3 | display: block;
4 | margin: 0;
5 | padding: 0;
6 | display: flex;
7 | justify-content: space-between;
8 | margin-bottom: 1.5rem;
9 | padding-bottom: 1.5rem;
10 | border-bottom: solid 1px var(--gray-white);
11 | }
12 |
13 | .listItem:last-of-type {
14 | border-bottom: none;
15 | margin-bottom: none;
16 | }
17 |
18 | .button {
19 | margin-left: 1.5rem;
20 | }
21 |
--------------------------------------------------------------------------------
/backend/.sequelizerc:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | //it's hard to use babel with sequelize, this is a crappy workaround
4 | //https://github.com/sequelize/cli/issues/112
5 | try {
6 | require('babel-register');
7 | } catch (e) {}
8 |
9 | const conf = require('./lib/conf').default;
10 |
11 |
12 | const NODE_ENV = process.env.NODE_ENV;
13 |
14 | const config = {
15 | 'migrations-path': 'lib/migrations',
16 | config: __filename
17 | };
18 |
19 | config[NODE_ENV] = conf.get('database');
20 |
21 | module.exports = config;
--------------------------------------------------------------------------------
/frontend/universal/modules/root.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { combineEpics } from 'redux-observable';
3 | import { routerReducer } from 'react-router-redux';
4 |
5 | // Epics
6 | import { counterEpics } from './counter/ducks/counter.js';
7 |
8 | // Reducers
9 | import * as Reducers from '../redux/reducers/index.js';
10 |
11 | export const rootEpic = combineEpics(
12 | ...counterEpics,
13 | );
14 |
15 | export default combineReducers({
16 | ...Reducers,
17 | routing: routerReducer,
18 | });
19 |
--------------------------------------------------------------------------------
/bin/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | source bin/env.sh
5 |
6 | #build frontend unless skipping explicitly
7 | if ! [[ $* == *--skipbuild* ]]; then
8 | ./bin/build_frontend.sh
9 | ./bin/npm_backend.sh i
10 | else
11 | echo "skipping frontend build..."
12 | fi
13 |
14 | if ! [[ $* == *--dontstop* ]]; then
15 | function finish {
16 | docker-compose -f docker-compose.test.yml stop
17 | }
18 | trap finish EXIT
19 | fi
20 |
21 | docker-compose -f docker-compose.test.yml run --rm testserver npm run test "$@"
22 |
23 |
24 |
--------------------------------------------------------------------------------
/bin/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #builds production images,
4 | #initializes database or creates db backup if it was initialized already
5 | #(re)starts prodcution containers
6 |
7 | source bin/env.sh
8 |
9 | #for init_db.sh
10 | export DOCKER_INIT_DB_CONFIG=$DOCKER_CONFIG_PROD
11 |
12 | if [ $(docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG ps | grep dbdata | wc -l) == 0 ]; then
13 | ./bin/init_db.sh
14 | else
15 | ./bin/backup.sh
16 | fi
17 | ./bin/build_production.sh
18 | ./bin/stop_production.sh
19 | ./bin/start_production.sh
--------------------------------------------------------------------------------
/frontend/universal/graphql/base.js:
--------------------------------------------------------------------------------
1 | import rxjs from 'rxjs';
2 | import { Observable } from 'rxjs/Observable';
3 |
4 | const domain = process.env.API_URL;
5 | const url = `${domain}/graphql`;
6 |
7 | export const get = (schema) => {
8 | return Observable.from(
9 | fetch(`${url}?query=${schema}`, {
10 | method: 'GET'
11 | }).then((data) => (data.json()))
12 | );
13 | }
14 |
15 | export const post = (schema) => {
16 | return Observable.from(
17 | fetch(`${url}?query=${schema}`, {
18 | method: 'POST',
19 | headers: {
20 | 'Content-Type': 'application/json'
21 | }
22 | }).then((data) => (data.json()))
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/docker-compose.test.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | dbtest:
4 | image: postgres:9.5
5 | environment:
6 | POSTGRES_DB: testdb
7 | POSTGRES_USER: testuser
8 | selenium:
9 | image: selenium/standalone-chrome-debug:2.53.0
10 | ports:
11 | - 5900:5900
12 | testserver:
13 | image: node:7.7
14 | working_dir: /backend
15 | command: npm run test
16 | volumes:
17 | - ./backend:/backend
18 | - ./frontend/dist:/static
19 | network_mode: "service:selenium"
20 | environment:
21 | NODE_ENV: test
22 | depends_on:
23 | - dbtest
24 | - selenium
--------------------------------------------------------------------------------
/docker-compose.development.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | db:
4 | ports:
5 | - "9432:5432"
6 | frontend:
7 | image: node:7.7
8 | working_dir: /frontend
9 | command: npm run develop
10 | volumes:
11 | - ./frontend:/frontend
12 | ports:
13 | - "3000:3000"
14 | backend:
15 | image: node:7.7
16 | working_dir: /backend
17 | command: npm run develop
18 | volumes:
19 | - ./backend:/backend
20 | - ./backend/media:/media
21 | ports:
22 | - "8000:8000"
23 | environment:
24 | NODE_ENV: ${NODE_ENV}
25 | depends_on:
26 | - db
27 |
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/CountersList/CountersList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import CounterListItem from 'universal/modules/counter/components/CounterListItem/CounterListItem.js';
4 |
5 | import styles from './counter-list.css';
6 |
7 | const CountersList = ({ counters, deleteCounter}) => (
8 |
9 | {counters.map((counter) => (
10 |
15 | ))}
16 |
17 | );
18 |
19 | export default CountersList;
20 |
--------------------------------------------------------------------------------
/frontend/universal/redux/createStore.prod.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | applyMiddleware
4 | compose,
5 | } from 'redux';
6 |
7 | import { routerMiddleware } from 'react-router-redux';
8 | import { createEpicMiddleware } from 'redux-observable';
9 | import { routerMiddleware } from 'react-router-redux';
10 |
11 | import reducers, { rootEpic } from '../modules/root.js';
12 |
13 | export default (history, initialState = {}) => {
14 | const epicMiddleware = createEpicMiddleware(rootEpic);
15 | const middleware = [routerMiddleware(history), epicMiddleware];
16 | const enhancer = compose(applyMiddleware(...middleware));
17 | const store = createStore(reducers, initialState, enhancer);
18 | return store;
19 | }
20 |
--------------------------------------------------------------------------------
/backend/tests/globals.js:
--------------------------------------------------------------------------------
1 | import app from '../lib/app';
2 | import {sequelize} from '../lib/models';
3 |
4 | let server;
5 |
6 | module.exports = {
7 |
8 | waitForConditionTimeout: 10000,
9 |
10 | before: function(done) {
11 | console.log('starting app');
12 | server = app.listen(8000, '0.0.0.0', () => done());
13 | },
14 |
15 | beforeEach: function(browser, done) {
16 | console.log('syncing db');
17 | sequelize.sync({force:true}).then(() => done());
18 | },
19 |
20 | after: function(done) {
21 | if (server) {
22 | console.log('stopping app');
23 | server.close();
24 | server = null;
25 | }
26 | done();
27 | }
28 | }
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/CounterListItem/CounterListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import cx from 'classnames';
4 |
5 | import styles from './counter-list-item.css';
6 | import { smallSquareButton } from 'universal/styles/button.css';
7 | import typeStyles from 'universal/styles/typography.css';
8 |
9 | const CounterListItem = ({ id, value, onDelete }) => (
10 |
11 | Counter | {value}
12 |
13 |
14 | );
15 |
16 | export default CounterListItem;
17 |
--------------------------------------------------------------------------------
/docker-compose.production.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | db:
4 | volumes:
5 | - "./logs/postgres:/var/log/postgresql"
6 | backend:
7 | build:
8 | context: ./backend
9 | environment:
10 | NODE_ENV: ${NODE_ENV_PROD}
11 | volumes:
12 | - "./logs/app:/tmp/logs/app"
13 | depends_on:
14 | - db
15 | nginx:
16 | build:
17 | context: ./
18 | dockerfile: ./nginx/Dockerfile
19 | depends_on:
20 | - backend
21 | volumes:
22 | - "./backend/media:/media"
23 | - "./logs/nginx:/tmp/logs"
24 | ports:
25 | - "80:80"
26 | - "443:443"
27 |
--------------------------------------------------------------------------------
/frontend/universal/components/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import { Link } from 'react-router-dom';
3 | import cx from 'classnames';
4 |
5 | import styles from './Home.css';
6 | import { mainButton } from 'universal/styles/button.css';
7 | import { title } from 'universal/styles/typography.css';
8 |
9 | class Home extends Component {
10 | render () {
11 | return (
12 |
13 |
⚡ Black Magic ⚡
14 |
15 | Go to App
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default Home;
23 |
--------------------------------------------------------------------------------
/frontend/webpack/dev-server.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import WebpackDevServer from 'webpack-dev-server';
3 | import config from './webpack.config.development';
4 |
5 | new WebpackDevServer(webpack(config), {
6 | publicPath: config.output.publicPath,
7 | filename: config.output.filename,
8 | inline: true,
9 | hot: true,
10 | stats: true,
11 | historyApiFallback: true,
12 | headers: {
13 | 'Access-Control-Allow-Origin': 'http://localhost:8000',
14 | 'Access-Control-Allow-Headers': 'X-Requested-With'
15 | }
16 | }).listen(3000, '0.0.0.0', function (err) {
17 | if (err) {
18 | console.error(err);
19 | } else {
20 | console.log('webpack dev server listening on localhost:3000');
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/frontend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "parserOptions": {
4 | "ecmaVersion": 6,
5 | "ecmaFeatures": {
6 | "jsx": true,
7 | "experimentalObjectRestSpread": true
8 | },
9 | "sourceType": "module"
10 | },
11 | "env": {
12 | "browser": true,
13 | "node": true
14 | },
15 | "plugins": [
16 | "react"
17 | ],
18 | "rules": {
19 | "no-console": 0,
20 | "strict": 0,
21 | "no-underscore-dangle": 0,
22 | "no-use-before-define": 0,
23 | "eol-last": 0,
24 | "quotes": [2, "single"],
25 | "jsx-quotes": 1,
26 | "react/jsx-no-undef": 1,
27 | "react/jsx-uses-react": 1,
28 | "react/jsx-uses-vars": 1
29 | }
30 | }
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/CountersPage/CountersPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import styles from './counters-page.css';
4 | import { squareButton } from 'universal/styles/button.css';
5 | import { title } from 'universal/styles/typography.css';
6 |
7 | import CountersList from 'universal/modules/counter/components/CountersList/CountersList.js';
8 |
9 | const CountersPage = ({ counters, createCounter, deleteCounter }) => (
10 |
11 |
12 | Counters
13 |
14 |
15 |
16 |
17 | );
18 |
19 | export default CountersPage;
20 |
--------------------------------------------------------------------------------
/backend/lib/migrations/20160425105021-create-counter.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: function(queryInterface, Sequelize) {
3 | return queryInterface.createTable('Counters', {
4 | id: {
5 | type: Sequelize.UUID,
6 | primaryKey: true
7 | },
8 | createdAt: {
9 | allowNull: false,
10 | type: Sequelize.DATE
11 | },
12 | updatedAt: {
13 | allowNull: false,
14 | type: Sequelize.DATE
15 | },
16 | value: {
17 | type: Sequelize.INTEGER,
18 | allowNull: false
19 | }
20 | });
21 | },
22 |
23 | down: function(queryInterface, Sequelize) {
24 | return queryInterface.dropTable('Counters');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | # db backups
36 | backups/*.bak
37 |
38 | # frontend build artefacts
39 | frontend/dist/*.js
40 | frontend/dist/*.css
41 | frontend/dist/*.map
42 | frontend/dist/*.json
43 | backend/reports
--------------------------------------------------------------------------------
/backend/config/production.json:
--------------------------------------------------------------------------------
1 | {
2 | "loggers": {
3 | "default": [
4 | {
5 | "transport": "File",
6 | "options": {
7 | "level": "info",
8 | "json": false,
9 | "timestamp": "true",
10 | "name": "app.log",
11 | "filename": "/tmp/logs/app/app_${time}.log"
12 | }
13 | }
14 | ],
15 | "sql": [
16 | {
17 | "transport": "File",
18 | "options": {
19 | "level": "info",
20 | "json": false,
21 | "timestamp": "true",
22 | "name": "app.log",
23 | "filename": "/tmp/logs/app/sql_${time}.log"
24 | }
25 | }
26 | ]
27 | }
28 | }
--------------------------------------------------------------------------------
/backend/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 8000,
3 | "host": "0.0.0.0",
4 |
5 | "serve_static_files": false,
6 | "webpack_dev_server": false,
7 |
8 | "database": {
9 | "username": "appuser",
10 | "database": "appdb",
11 | "password": null,
12 | "host": "db",
13 | "port": 5432,
14 | "dialect": "postgres"
15 | },
16 |
17 | "loggers": {
18 | "default": [
19 | {
20 | "transport": "Console",
21 | "options": {
22 | "level": "info",
23 | "timestamp": "true"
24 | }
25 | }
26 | ],
27 | "sql": [
28 | {
29 | "transport": "Console",
30 | "options": {
31 | "level": "info",
32 | "timestamp": "true"
33 | }
34 | }
35 | ]
36 | }
37 | }
--------------------------------------------------------------------------------
/bin/restore.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # restore a bacakup. arg is a filename that exitsts in backups dir
3 |
4 | source bin/env.sh
5 |
6 | BACKUP_FILE=$1
7 |
8 | if [ -z "$BACKUP_FILE" ]; then
9 | echo "one argument required - backup file to restore"
10 | exit 1
11 | fi
12 |
13 | if ! [ -f $BACKUP_FILE ]; then
14 | echo "file $BACKUP_FILE not found"
15 | exit 1
16 | fi
17 |
18 | if [ $(docker-compose -f docker-compose.yml -f $DOCKER_CONFIG_PROD -f docker-compose.db.yml ps | grep "_db_" | grep "Up" | wc -l) != 0 ]; then
19 | echo "database container running. please stop before trying to restore"
20 | exit 1
21 | fi
22 |
23 | echo "restoring database..."
24 | dcprod -f docker-compose.db.yml run --rm dbclient bash -c 'dropdb -h db -U $POSTGRES_USER $POSTGRES_DB && createdb -h db -U $POSTGRES_USER -O $POSTGRES_USER $POSTGRES_DB && pg_restore -Fc -h db -U $POSTGRES_USER -d $POSTGRES_DB '"$BACKUP_FILE"
25 | echo "restore complete"
--------------------------------------------------------------------------------
/backend/lib/conf.js:
--------------------------------------------------------------------------------
1 | import nconf from 'nconf';
2 | import path from 'path';
3 | import fs from 'fs';
4 |
5 | //default config
6 | const defaultconfpath = path.join(process.cwd(), 'config', 'default.json');
7 |
8 | //environment config
9 | const confpath = path.join(process.cwd(), 'config', `${process.env.NODE_ENV}.json`);
10 |
11 |
12 | //if config file for this env exists, load it then fill in defaults from default.json
13 |
14 | let env_conf_exists = false;
15 | try {
16 | fs.accessSync(confpath, fs.R_OK);
17 | env_conf_exists = true;
18 | } catch (e) {
19 |
20 | }
21 |
22 | if (env_conf_exists) {
23 | nconf.file(confpath);
24 | nconf.defaults({
25 | type: 'file',
26 | file: defaultconfpath
27 | });
28 |
29 | //else load default.json only
30 | } else {
31 | console.warn(`config file not found for env ${process.env.NODE_ENV}`);
32 | nconf.file(defaultconfpath);
33 | }
34 |
35 | export default nconf;
--------------------------------------------------------------------------------
/frontend/universal/routes/Routes.js:
--------------------------------------------------------------------------------
1 | // Libraries
2 | import React from 'react';
3 | import { Route, Switch } from 'react-router';
4 |
5 | // Containers
6 | import AppContainer from 'universal/containers/App/AppContainer.js';
7 |
8 | // Routes
9 | // For Development only
10 | import * as RouteMap from '../routes/static.js';
11 |
12 | // This is used in production for code splitting via `wepback.config.server.js`
13 | // import * as RouteMap from '../routes/async.js';
14 |
15 | const Routes = ({ history }) => (
16 |
17 | (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )} />
26 |
27 | );
28 |
29 |
30 | export default Routes;
31 |
--------------------------------------------------------------------------------
/backend/nightwatch.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_folders" : ["tests/specs"],
3 | "output_folder" : "reports",
4 | "custom_commands_path" : "",
5 | "custom_assertions_path" : "",
6 | "page_objects_path" : "",
7 | "globals_path" : "tests/globals.js",
8 |
9 | "selenium" : {
10 | "start_process" : false,
11 | "cli_args" : {
12 | "webdriver.chrome.driver" : "",
13 | "webdriver.ie.driver" : ""
14 | }
15 | },
16 |
17 | "test_settings" : {
18 | "default" : {
19 | "launch_url" : "http://localhost:8000",
20 | "selenium_port" : 4444,
21 | "selenium_host" : "localhost",
22 | "desiredCapabilities": {
23 | "browserName": "chrome",
24 | "javascriptEnabled": true,
25 | "acceptSslCerts": true
26 | }
27 | },
28 |
29 | "chrome" : {
30 | "desiredCapabilities": {
31 | "browserName": "chrome",
32 | "javascriptEnabled": true,
33 | "acceptSslCerts": true
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/backend/lib/models/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import Sequelize from 'sequelize';
4 | import conf from '../conf';
5 | import { getLogger } from '../logger';
6 |
7 |
8 | const dbconf = conf.get('database');
9 | const sqllogger = getLogger('sql');
10 |
11 | export const sequelize = new Sequelize(dbconf.database, dbconf.username, dbconf.password,
12 | {
13 | ...dbconf,
14 | logging: sqllogger.info.bind(sqllogger)
15 | }
16 | );
17 |
18 | const models = {};
19 |
20 | //load models
21 | fs
22 | .readdirSync(__dirname)
23 | .filter(function(file) {
24 | return file !== 'index.js';
25 | })
26 | .forEach(function(file) {
27 | const model = sequelize.import(path.join(__dirname, file));
28 | models[model.name] = model;
29 | });
30 |
31 | //create associations
32 | Object.keys(models).forEach(function(name) {
33 | if ('associate' in models[name]) {
34 | models[name].associate(models);
35 | }
36 | });
37 |
38 | export default models;
39 |
--------------------------------------------------------------------------------
/frontend/universal/routes/async.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function asyncRoute(getComponent) {
4 | return class AsyncComponent extends React.Component {
5 | state = {
6 | Component: null
7 | };
8 |
9 | componentDidMount() {
10 | if ( this.state.Component === null ) {
11 | getComponent().then((Component) => {
12 | this.setState({Component: Component});
13 | })
14 | }
15 | }
16 |
17 | render() {
18 | const {
19 | Component
20 | } = this.state;
21 |
22 | if ( Component ) {
23 | return ();
24 | }
25 | return (loading...
); // or with a loading spinner, etc..
26 | }
27 | }
28 | }
29 |
30 | export const Home = asyncRoute(() => {
31 | return System.import('../components/Home/Home.js');
32 | });
33 |
34 | export const Counter = asyncRoute(() => {
35 | return System.import('../modules/counter/containers/Counter/CounterContainer.js');
36 | });
37 |
--------------------------------------------------------------------------------
/frontend/universal/redux/createStore.dev.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | applyMiddleware,
4 | compose,
5 | } from 'redux';
6 |
7 | import { routerMiddleware } from 'react-router-redux';
8 | import { createEpicMiddleware } from 'redux-observable';
9 |
10 | import reducers, { rootEpic } from '../modules/root.js';
11 |
12 | export default (history, initialState = {}) => {
13 | const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
14 |
15 | const epicMiddleware = createEpicMiddleware(rootEpic);
16 | const middleware = [routerMiddleware(history), epicMiddleware];
17 | const enhancer = compose(applyMiddleware(...middleware), devTools);
18 |
19 | const store = createStore(reducers, initialState, enhancer);
20 |
21 | if (module.hot) {
22 | // Enable Webpack hot module replacement for reducers
23 | module.hot.accept('./reducers', () => {
24 | const nextReducers = require('../modules/root.js');
25 | store.replaceReducer(nextReducer);
26 | });
27 | }
28 |
29 | return store;
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/universal/components/App/App.css:
--------------------------------------------------------------------------------
1 | html {
2 | min-height: 100%;
3 | background: linear-gradient(90deg, #80A2CC, #F99F86);
4 | background: -webkit-linear-gradient(90deg, #80A2CC, #F99F86);
5 | }
6 |
7 | body {
8 | min-height: 100%;
9 | height: 100vmax;
10 | margin: 0;
11 | padding: 0;
12 | display: flex;
13 | flex-direction: column;
14 | flex: 1;
15 | position: relative;
16 | justify-content: center;
17 | align-items: center;
18 | /*font-family: 'Arvo', Arial, Helvetica, Sans-serif;*/
19 | background: linear-gradient(90deg, rgba(53, 66, 99, 0.5), rgba(255,255,248,0.7));
20 | background: -webkit-linear-gradient(90deg, rgba(53, 66, 99, 0.5), rgba(255,255,248,0.7));
21 | }
22 |
23 | h1,h2,h3,h4,h5,h6 {
24 | color: white;
25 | }
26 |
27 | h2 {
28 | font-size: 2em;
29 | line-height: 1.5em;
30 | }
31 |
32 | p, li, ul, ol {
33 | font-family: 'Raleway';
34 | color: white;
35 | font-size: 20px;
36 | line-height: 1.25em;
37 | }
38 |
39 | .app {
40 | margin-bottom: 150px;
41 | margin-top: 150px;
42 | }
43 |
--------------------------------------------------------------------------------
/backend/lib/logger.js:
--------------------------------------------------------------------------------
1 |
2 | import fsExtra from 'fs-extra';
3 | import winston from 'winston';
4 | import conf from './conf';
5 |
6 | const loggers = {};
7 |
8 |
9 | export function getLogger(name) {
10 |
11 | if (loggers[name]) {
12 | return loggers[name];
13 | }
14 |
15 | const transports = (conf.get('loggers')[name] || []).map(tconf => {
16 | const options = {
17 | ...tconf.options
18 | };
19 |
20 | //if logging to file, insert timestamp & make sure file exists
21 | if (options.filename) {
22 | options.filename = options.filename.replace('${time}', '_' +(new Date().toISOString()).substr(0, 19).replace('T', '_').replace(/\:/g, '-'));
23 | fsExtra.ensureFileSync(options.filename);
24 | }
25 | return new (winston.transports[tconf.transport])(options);
26 | });
27 |
28 | const logger = new (winston.Logger)({
29 | transports: transports
30 | });
31 |
32 | loggers[name] = logger;
33 | return logger;
34 | }
35 |
36 | export default getLogger('default');
--------------------------------------------------------------------------------
/bin/init_db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #initialize database if it hasn't been initialized yet
4 |
5 | DOCKER_INIT_DB_CONFIG=${DOCKER_INIT_DB_CONFIG:-$DOCKER_CONFIG_DEV}
6 |
7 | echo "inti db config: $DOCKER_INIT_DB_CONFIG"
8 |
9 | if [ $(docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG ps | grep dbdata | wc -l) == 0 ]; then
10 | echo "initializing database"
11 | docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG up -d db
12 | DB_CONTAINER_ID=$(docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG ps -q db)
13 | for i in {30..0}; do
14 | echo "waiting for postgres to finish initializing..."
15 | if [ $(docker logs $DB_CONTAINER_ID 2>&1 | grep "database system is ready to accept connections" | wc -l) == 1 ]; then
16 | docker-compose -f docker-compose.yml -f $DOCKER_INIT_DB_CONFIG stop db
17 | echo "db initialized"
18 | exit 0
19 | else
20 | sleep 1
21 | fi
22 | done
23 | if [ "$i" = 0 ]; then
24 | echo >&2 "db init failed"
25 | exit 1
26 | fi
27 | fi
28 |
--------------------------------------------------------------------------------
/backend/lib/app.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import bodyParser from 'body-parser';
3 |
4 | import graphqlHTTP from 'express-graphql';
5 | import schema from './graphql';
6 |
7 | import logger from './logger';
8 | import conf from './conf';
9 | import models, { sequelize } from './models';
10 | import {
11 | renderDevPage,
12 | renderPage,
13 | } from './ssr.js';
14 |
15 | const PROD = process.env.NODE_ENV === 'production';
16 |
17 | const PORT = conf.get('port');
18 | const HOST = conf.get('host');
19 |
20 | logger.info(`initializing app with NODE_ENV=${process.env.NODE_ENV}`);
21 |
22 | const app = express();
23 |
24 | app.use(bodyParser.json());
25 |
26 | //used for tests
27 | if (conf.get('serve_static_files')) {
28 | app.use('/static', express.static('/static'));
29 | }
30 |
31 | app.post('/graphql', graphqlHTTP({
32 | schema: schema,
33 | graphiql: false
34 | }));
35 |
36 | app.get('/graphql', graphqlHTTP({
37 | schema: schema,
38 | graphiql: !PROD
39 | }));
40 |
41 | const render = PROD ? renderPage : renderDevPage;
42 |
43 | app.get('*', render);
44 |
45 | export default app;
46 |
--------------------------------------------------------------------------------
/frontend/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { ConnectedRouter } from 'react-router-redux';
5 | import createHistory from 'history/createBrowserHistory';
6 | import createStore from './universal/redux/createStore';
7 |
8 | // Styles
9 | import 'universal/styles/colors.css';
10 | import 'universal/styles/reset.css';
11 |
12 | import Routes from './universal/routes/Routes.js';
13 |
14 | const history = createHistory();
15 | const store = createStore(history);
16 |
17 | const MOUNT_NODE = document.getElementById('mount');
18 |
19 | const renderApp = (Component) => {
20 | render(
21 |
22 |
23 |
24 |
25 | ,
26 | MOUNT_NODE
27 | );
28 | }
29 |
30 | renderApp(Routes);
31 |
32 | if (module.hot) {
33 | module.hot.accept('./universal/routes/Routes.js', () => {
34 | const nextRoutes = require('./universal/routes/Routes.js');
35 | renderApp(nextRoutes);
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Producters
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/backend/lib/ssr.js:
--------------------------------------------------------------------------------
1 | // Node Modules
2 | import fs from 'fs';
3 | import {basename, join} from 'path';
4 |
5 | // Libraries
6 | import React from 'react';
7 | import {renderToString} from 'react-dom/server';
8 |
9 | // Redux
10 | import createHistory from 'history/createMemoryHistory'
11 |
12 | // Components
13 | import Html from './Html.js';
14 |
15 | const PROD = process.env.NODE_ENV === 'production';
16 |
17 | const assets = PROD ? require('./dist/assets.json') : {};
18 | const createStore = PROD ? require('./dist/store.js') : () => {};
19 |
20 |
21 | function renderApp(url, res, store, assets) {
22 | const context = {};
23 |
24 | const html = renderToString(
25 |
58 |
59 | {PROD ? : }
60 | {PROD && }
61 | {PROD &&
31 | );
32 |
33 | res.send(''+html);
34 | }
35 |
36 | export const renderDevPage = function (req, res) {
37 | renderApp(req.url, res);
38 | }
39 |
40 | export const renderPage = function (req, res) {
41 | const history = createHistory( );
42 | const store = createStore(history);
43 |
44 | assets.manifest.text = fs.readFileSync(join(__dirname, assets.manifest.js), 'utf-8');
45 |
46 | renderApp(req.url, res, store, assets);
47 | };
48 |
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/containers/Counters/CountersContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import {
5 | counterFetchCounters,
6 | counterDeleteCounter,
7 | counterCreateCounter,
8 | counterSortedCountersSelector,
9 | } from 'universal/modules/counter/ducks/counter.js';
10 |
11 | import CountersPage from 'universal/modules/counter/components/CountersPage/CountersPage.js';
12 |
13 | class CountersContainer extends Component {
14 | componentDidMount () {
15 | this.props.fetchCounters();
16 | }
17 |
18 | render () {
19 | const {
20 | counters,
21 | deleteCounter,
22 | createCounter,
23 | } = this.props;
24 |
25 | return (
26 |
27 | );
28 | }
29 | }
30 |
31 | function mapStateToProps (state, ownProps) {
32 | return {
33 | counters: counterSortedCountersSelector(state, ownProps).toArray(),
34 | };
35 | }
36 |
37 | function mapDispatchToProps (dispatch) {
38 | return {
39 | fetchCounters: () => (dispatch(counterFetchCounters())),
40 | deleteCounter: id => (dispatch(counterDeleteCounter(id))),
41 | createCounter: () => (dispatch(counterCreateCounter())),
42 | };
43 | }
44 |
45 | export default connect(mapStateToProps, mapDispatchToProps)(CountersContainer) ;
46 |
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/Counter/Counter.css:
--------------------------------------------------------------------------------
1 | .counterContainer {
2 | position: relative;
3 | width: 225px;
4 | margin: auto;
5 | }
6 |
7 | .counter {
8 | width: 200px;
9 | height: 200px;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | font-size: 4em;
14 | border: solid 10px white;
15 | border-radius: 200px;
16 | color: white;
17 | }
18 |
19 | .button {
20 | display: inline-block;
21 | display: flex;
22 | font-size: 3em;
23 | line-height: 50px;
24 | justify-content: center;
25 | vertical-align: center;
26 | color: white;
27 | background-color: #30B661;
28 | border: solid 1px #0F9D58;
29 | border-radius: 50px;
30 | width: 50px;
31 | height: 50px;
32 | -webkit-user-select: none; /* Chrome all / Safari all */
33 | }
34 |
35 | .button:hover {
36 | cursor: pointer;
37 | }
38 |
39 | .positive {
40 | background-color: #B1DDCC;
41 | border: solid 7px #7EC6AB;
42 | position: absolute;
43 | right: -25px;
44 | top: 50%;
45 | margin-top: -25px;
46 | font-size: 2.5em
47 | }
48 |
49 | .positive:hover {
50 | /*background-color: #DEF0EA;*/
51 | border-color: #B1DDCC;
52 | }
53 |
54 | .negative {
55 | background-color: #F48F94;
56 | border: solid 7px #F27D83;
57 | position: absolute;
58 | left: -25px;
59 | top: 50%;
60 | margin-top: -25px;
61 | line-height: 45px;
62 | }
63 |
64 | .negative:hover {
65 | border-color: #F48F94;
66 | }
67 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "develop": "nodemon lib/index.js --exec babel-node",
7 | "compile": "babel lib -d dist",
8 | "serve": "node dist/index.js",
9 | "test": "babel-node ./node_modules/.bin/nightwatch -c ./nightwatch.json"
10 | },
11 | "author": "domas@producters.com",
12 | "license": "MIT",
13 | "dependencies": {
14 | "babel-cli": "^6.24.0",
15 | "babel-preset-env": "^1.6.0",
16 | "babel-preset-react": "^6.24.1",
17 | "babel-preset-stage-0": "^6.22.0",
18 | "babel-register": "^6.24.0",
19 | "body-parser": "^1.17.1",
20 | "express": "^4.15.2",
21 | "express-graphql": "^0.6.11",
22 | "fs-extra": "^2.1.2",
23 | "graphql": "^0.8.0",
24 | "graphql-sequelize": "^5.4.2",
25 | "graphql-tools": "^2.5.1",
26 | "graphql-tools-sequelize": "^1.3.11",
27 | "graphql-tools-types": "^1.1.12",
28 | "history": "^4.7.2",
29 | "immutable": "^3.8.1",
30 | "nconf": "^0.8.4",
31 | "pg": "^6.1.4",
32 | "react": "^15.6.1",
33 | "react-dom": "^15.6.1",
34 | "react-redux": "^5.0.6",
35 | "react-router": "^4.2.0",
36 | "react-router-redux": "^4.0.8",
37 | "redux": "^3.7.2",
38 | "sequelize": "^3.30.2",
39 | "sequelize-cli": "^2.6.0",
40 | "winston": "^2.3.1"
41 | },
42 | "devDependencies": {
43 | "nightwatch": "^0.9.13",
44 | "nodemon": "^1.11.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/backend/lib/api/notes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import models from '../models';
3 | import logger from '../logger';
4 |
5 | const router = express.Router();
6 |
7 |
8 | //list
9 | router.get('/notes/', function(req, res) {
10 | models.notes.findAll()
11 | .then(function(notes) {
12 | res.json(notes.map(note => note.toJSON()));
13 | })
14 | .catch(function(e) {
15 | logger.error('error fetching notes', e);
16 | res.status(500).json({});
17 | });
18 | });
19 |
20 | //delete
21 | router.delete('/notes/:id', function(req, res) {
22 | models.notes.findById(req.params.id)
23 | .then(function(note) {
24 | if (note) {
25 | return note.destroy()
26 | .then(function() {
27 | res.status(200).json({});
28 | });
29 | }
30 |
31 | res.status(404).json({});
32 | })
33 | .catch(function(err) {
34 | logger.error('error destroying note', err);
35 | res.status(500).json({});
36 | });
37 | })
38 |
39 | //create
40 | router.post('/notes/', function(req, res) {
41 | logger.info('creating note', JSON.stringify(req.body));
42 |
43 | models.notes.create(req.body)
44 | .then(function(note) {
45 | res.status(201).json(note.toJSON());
46 | })
47 | .catch(function(err) {
48 | logger.error('error creating note', err);
49 | res.status(500).json();
50 | });
51 | })
52 |
53 | export default router;
--------------------------------------------------------------------------------
/frontend/universal/graphql/counter.js:
--------------------------------------------------------------------------------
1 | import { get, post } from './base.js';
2 |
3 | export const countersQuerySchema = ({ limit = 100 }) => (`
4 | query {
5 | Counters(limit: ${limit}) {
6 | id,
7 | value,
8 | createdAt,
9 | }
10 | }
11 | `);
12 |
13 | export const counterQuerySchema = ({ id }) => (`
14 | query {
15 | Counter(id: ${id}) {
16 | id,
17 | value,
18 | }
19 | }
20 | `);
21 |
22 | export const updateCounterMutation = ({ id, value }) => (`
23 | mutation {
24 | Counter(id: "${id}") {
25 | update(with: {value: ${value}}) {
26 | id,
27 | value,
28 | }
29 | }
30 | }
31 | `);
32 |
33 | export const createCounterMutation = ({ value = 0 }) => (`
34 | mutation {
35 | Counter {
36 | create(with:{value: ${value}}) {
37 | id,
38 | value,
39 | }
40 | }
41 | }
42 | `);
43 |
44 | export const deleteCounterMutation = ({ id }) => (`
45 | mutation {
46 | Counter(id: "${id}") {
47 | delete
48 | }
49 | }
50 | `);
51 |
52 | export const getCounters = () => (get(countersQuerySchema({ limit: 100 })));
53 | export const getCounter = (id) => (get(counterQuerySchema({ id })));
54 | export const createCounter = (value) => (post(createCounterMutation({ value })));
55 | export const deleteCounter = (id) => (post(deleteCounterMutation({ id })));
56 | export const updateCounter = (id, value) => (post(updateCounterMutation({ id, value })));
57 |
--------------------------------------------------------------------------------
/nginx/nginx_nossl.conf:
--------------------------------------------------------------------------------
1 | worker_processes 1;
2 |
3 | user nobody nogroup;
4 | pid /tmp/nginx.pid;
5 | error_log /tmp/logs/nginx.error.log;
6 |
7 | events {
8 | worker_connections 1024;
9 | accept_mutex off;
10 | }
11 |
12 | http {
13 | include mime.types;
14 | default_type application/octet-stream;
15 | access_log /tmp/logs/nginx.access.log combined;
16 | sendfile on;
17 |
18 | server {
19 | listen 80 default;
20 | client_max_body_size 4G;
21 | server_name _;
22 |
23 |
24 | gzip on;
25 | gzip_disable "msie6";
26 | gzip_vary on;
27 | gzip_proxied any;
28 | gzip_comp_level 6;
29 | gzip_buffers 16 8k;
30 | gzip_http_version 1.1;
31 | gzip_types application/javascript text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
32 |
33 | keepalive_timeout 5;
34 |
35 | location /static/ {
36 | alias /static/;
37 | }
38 |
39 | location /media/ {
40 | alias /media/;
41 | }
42 |
43 | location / {
44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
45 | proxy_set_header Host $http_host;
46 |
47 | # UNCOMMENT LINE BELOW IF THIS IS BEHIND A SSL PROXY
48 | #proxy_set_header X-Forwarded-Proto https;
49 |
50 | proxy_redirect off;
51 | proxy_pass http://backend:8000;
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/components/Counter/Counter.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import styles from './Counter.css';
3 | import classNames from 'classnames';
4 |
5 | class Counter extends Component {
6 |
7 | static propTypes = {
8 | id: PropTypes.string.isRequired,
9 | count: PropTypes.number.isRequired,
10 | incrementCount: PropTypes.func.isRequired,
11 | decrementCount: PropTypes.func.isRequired,
12 | }
13 |
14 | handleLinkClick(event) {
15 | event.stopPropagation();
16 | event.preventDefault();
17 | }
18 |
19 | handleIncrementClick (incrementCount, event) {
20 | const {
21 | id,
22 | count,
23 | } = this.props;
24 |
25 | this.handleLinkClick(event);
26 | incrementCount(id, count);
27 | }
28 |
29 | handleDecrementClick(decrementCount, event) {
30 | const {
31 | id,
32 | count,
33 | } = this.props;
34 |
35 | this.handleLinkClick(event);
36 | decrementCount(id, count);
37 | }
38 |
39 | render () {
40 | const {
41 | count,
42 | incrementCount,
43 | decrementCount
44 | } = this.props;
45 |
46 | return (
47 |
48 |
{count}
49 |
+
50 |
-
51 |
52 | )
53 | }
54 | }
55 |
56 | export default Counter;
57 |
--------------------------------------------------------------------------------
/frontend/universal/modules/counter/containers/Counter/CounterContainer.js:
--------------------------------------------------------------------------------
1 | // Libraries
2 | import React, {Component, PropTypes} from 'react';
3 | import {connect} from 'react-redux';
4 |
5 | // Components
6 | import Counter from '../../components/Counter/Counter.js';
7 |
8 | // Actions
9 | import {
10 | counterSetCount,
11 | counterCurrentCounterCountSelector,
12 | } from '../../ducks/counter.js';
13 |
14 | import {
15 | locationCurrentCounterId,
16 | } from 'universal/modules/location/ducks/location';
17 |
18 | class CounterContainer extends Component {
19 | render () {
20 | const {
21 | id,
22 | count,
23 | incrementCount,
24 | decrementCount,
25 | } = this.props;
26 | return ();
27 | }
28 | }
29 |
30 | CounterContainer.propTypes = {
31 | // State
32 | id: PropTypes.string.isRequired,
33 | count: PropTypes.number.isRequired,
34 |
35 | // Dispatchers
36 | incrementCount: PropTypes.func.isRequired,
37 | decrementCount: PropTypes.func.isRequired
38 | };
39 |
40 |
41 | function mapStateToProps(state, ownProps) {
42 | return {
43 | id: locationCurrentCounterId(state, ownProps),
44 | count: counterCurrentCounterCountSelector(state, ownProps),
45 | };
46 | }
47 |
48 | function mapDispatchToProps(dispatch, props) {
49 | return {
50 | incrementCount: (id, count) => dispatch(counterSetCount(id, count + 1)),
51 | decrementCount: (id, count) => dispatch(counterSetCount(id, count - 1))
52 | };
53 | }
54 |
55 | export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
56 |
--------------------------------------------------------------------------------
/frontend/universal/styles/button.css:
--------------------------------------------------------------------------------
1 | .mainButton {
2 | font-family: var(--font-family);
3 | background-color: var(--bone-white);
4 | font-size: 1.6rem;
5 | border: solid 0.25rem rgba(53, 66, 99, 0.95);
6 | box-shadow: var(--midnight-blue) 0px 1px 2px;
7 | transition: all 250ms ease-in-out;
8 |
9 | padding: 1rem 2rem;
10 | border-radius: 3rem;
11 | text-align: center;
12 | text-decoration: none;
13 | color: var(--midnight-blue);
14 | }
15 |
16 | .mainButton:hover {
17 | cursor: pointer;
18 | border: solid 0.25rem rgba(53, 66, 99, 1);
19 | box-shadow: rgba(53, 66, 99, 0.95) 0px 2px 5px;
20 | }
21 |
22 | .squareButton {
23 | font-family: var(--font-family);
24 | background-color: var(--dust-white);
25 | font-size: 1.6rem;
26 | transition: all 250ms ease-in-out;
27 | border: none;
28 |
29 | padding: 1rem 2rem;
30 | border-radius: 0.25rem;
31 | text-align: center;
32 | text-decoration: none;
33 | color: var(--midnight-blue);
34 | }
35 |
36 | .squareButton:active {
37 | outline: none;
38 | }
39 |
40 | .squareButton:hover {
41 | cursor: pointer;
42 | background-color: var(--gray-white);
43 | }
44 |
45 | .smallSquareButton {
46 | font-family: var(--font-family);
47 | background-color: var(--dust-white);
48 | font-size: 1.2rem;
49 | transition: all 250ms ease-in-out;
50 | border: none;
51 |
52 | padding: 0.5rem 1.5rem;
53 | border-radius: 0.25rem;
54 | text-align: center;
55 | text-decoration: none;
56 | color: var(--midnight-blue);
57 | }
58 |
59 | .smallSquareButton:active {
60 | outline: none;
61 | }
62 |
63 | .smallSquareButton:hover {
64 | cursor: pointer;
65 | background-color: var(--gray-white);
66 | }
67 |
--------------------------------------------------------------------------------
/backend/lib/graphql/index.js:
--------------------------------------------------------------------------------
1 | import GraphQLToolsSequelize from "graphql-tools-sequelize";
2 | import GraphQLToolsTypes from "graphql-tools-types";
3 | import { makeExecutableSchema } from "graphql-tools";
4 |
5 | import { sequelize } from '../models';
6 |
7 | const graphQLTools = new GraphQLToolsSequelize(sequelize);
8 | graphQLTools.boot();
9 |
10 | const typeDefs = `
11 | schema {
12 | query: Root
13 | mutation: Root
14 | }
15 | scalar UUID
16 | scalar JSON
17 | scalar Date
18 | type Root {
19 | ${graphQLTools.entityQuerySchema("Root", "", "Counter")}
20 | ${graphQLTools.entityQuerySchema("Root", "", "Counter*")}
21 | }
22 | type Counter {
23 | id: UUID!
24 | value: Int
25 | createdAt: Date
26 | updatedAt: Date
27 | ${graphQLTools.entityCloneSchema("Counter")}
28 | ${graphQLTools.entityCreateSchema("Counter")}
29 | ${graphQLTools.entityUpdateSchema("Counter")}
30 | ${graphQLTools.entityDeleteSchema("Counter")}
31 | }
32 | `;
33 |
34 | const resolvers = {
35 | UUID: GraphQLToolsTypes.UUID({ name: "UUID", storage: "string" }),
36 | JSON: GraphQLToolsTypes.JSON({ name: "JSON" }),
37 | Date: GraphQLToolsTypes.Date({ name: "Date" }),
38 |
39 | Root: {
40 | Counter: graphQLTools.entityQueryResolver("Root", "", "Counter"),
41 | Counters: graphQLTools.entityQueryResolver("Root", "", "Counter*"),
42 | },
43 | Counter: {
44 | create: graphQLTools.entityCreateResolver("Counter"),
45 | update: graphQLTools.entityUpdateResolver("Counter"),
46 | delete: graphQLTools.entityDeleteResolver("Counter")
47 | }
48 |
49 | };
50 |
51 | const schema = makeExecutableSchema({
52 | typeDefs: [ typeDefs ],
53 | resolvers: resolvers
54 | });
55 |
56 | export default schema;
57 |
--------------------------------------------------------------------------------
/backend/lib/Html.js:
--------------------------------------------------------------------------------
1 | // Libraries
2 | import React, {Component, PropTypes} from 'react';
3 | import path from 'path';
4 | import {StaticRouter} from 'react-router';
5 | import {renderToString} from 'react-dom/server';
6 |
7 | // Redux
8 | import { Provider } from 'react-redux';
9 |
10 | const PROD = process.env.NODE_ENV === 'production';
11 |
12 | class Html extends Component {
13 | static propTypes = {
14 | url: PropTypes.string.isRequired,
15 | title: PropTypes.string.isRequired,
16 | store: PropTypes.object,
17 | assets: PropTypes.object
18 | }
19 |
20 | render () {
21 | const {
22 | title,
23 | assets,
24 | store,
25 | url,
26 | context
27 | } = this.props;
28 |
29 | const {
30 | manifest,
31 | bundle,
32 | vendor,
33 | prerender,
34 | } = assets || {};
35 |
36 | const state = PROD ? JSON.stringify(store.getState()) : JSON.stringify({});
37 |
38 | const initialState = `window.__INITIAL_STATE__ = ${state}`;
39 | const Layout = PROD ? require( path.join(__dirname,'dist', 'prerender.js')) : () => {};
40 |
41 | const root = PROD && renderToString(
42 |
43 |
44 |
45 |
46 |
47 | );
48 |
49 | return (
50 |
51 |
52 |
53 |
{title}
54 | {PROD && }
55 | {PROD && }
56 |
57 |