├── 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 | 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 | 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 | 58 |