├── .gitignore ├── .gitlab-ci.yml ├── README.md ├── client ├── .babelrc ├── .dockerignore ├── .eslintrc ├── Dockerfile ├── index.html ├── package.json ├── serve.js ├── src │ ├── app │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── app.test.js.snap │ │ │ └── app.test.js │ │ └── index.js │ ├── components │ │ ├── footer │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── footer.test.js.snap │ │ │ │ └── footer.test.js │ │ │ └── index.js │ │ ├── navbar │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── navbar.test.js.snap │ │ │ │ └── navbar.test.js │ │ │ └── index.js │ │ ├── notifications │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── notification.test.js.snap │ │ │ │ │ └── notifications.test.js.snap │ │ │ │ ├── notification.test.js │ │ │ │ └── notifications.test.js │ │ │ ├── index.js │ │ │ ├── notification.js │ │ │ ├── notifications.js │ │ │ └── transitions.css │ │ ├── question │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── question.test.js.snap │ │ │ │ └── question.test.js │ │ │ └── index.js │ │ └── user │ │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── user.test.js.snap │ │ │ └── user.test.js │ │ │ └── index.js │ ├── index.js │ ├── pages │ │ ├── create │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── create.test.js.snap │ │ │ │ └── create.test.js │ │ │ └── index.js │ │ ├── home │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── home.test.js.snap │ │ │ │ └── home.test.js │ │ │ └── index.js │ │ ├── login │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── login.test.js.snap │ │ │ │ └── login.test.js │ │ │ └── index.js │ │ ├── notfound │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── notfound.test.js.snap │ │ │ │ └── notfound.test.js │ │ │ └── index.js │ │ ├── profile │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── profile.test.js.snap │ │ │ │ └── profile.test.js │ │ │ └── index.js │ │ └── register │ │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── register.test.js.snap │ │ │ └── register.test.js │ │ │ └── index.js │ ├── store │ │ ├── actionTypes.js │ │ ├── actions │ │ │ └── index.js │ │ ├── epics │ │ │ ├── __tests__ │ │ │ │ ├── auth.test.js │ │ │ │ ├── notifications.test.js │ │ │ │ ├── questions.test.js │ │ │ │ └── users.test.js │ │ │ ├── auth.js │ │ │ ├── helloworld.js │ │ │ ├── index.js │ │ │ ├── notifications.js │ │ │ ├── questions.js │ │ │ └── users.js │ │ ├── index.js │ │ ├── reducers │ │ │ ├── __tests__ │ │ │ │ ├── auth.test.js │ │ │ │ ├── notifications.test.js │ │ │ │ ├── questions.test.js │ │ │ │ └── users.test.js │ │ │ ├── auth.js │ │ │ ├── helloworld.js │ │ │ ├── index.js │ │ │ ├── notifications.js │ │ │ ├── questions.js │ │ │ └── users.js │ │ ├── rootEpic.js │ │ └── rootReducer.js │ └── util │ │ ├── __tests__ │ │ ├── errorToMessage.test.js │ │ ├── requireAuth.test.js │ │ └── signRequest.test.js │ │ ├── errorToMessage.js │ │ ├── index.js │ │ ├── requireAuth.js │ │ └── signRequest.js ├── test │ └── setup.js ├── webpack.config.js └── yarn.lock ├── deploy ├── Makefile ├── docker-compose-img.yml └── docker-compose.yml └── server ├── .dockerignore ├── .eslintrc ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── config.js ├── index.js ├── package.json ├── src ├── app.js ├── auth │ ├── index.js │ ├── login.js │ ├── passport.js │ └── register.js ├── db │ ├── index.js │ ├── question.js │ ├── thinky.js │ └── user.js ├── index.js ├── question │ ├── answer.js │ ├── create.js │ ├── delete.js │ ├── get.js │ ├── index.js │ └── update.js ├── user │ ├── get.js │ ├── index.js │ └── update.js └── util │ ├── asyncRequest.js │ ├── hash.js │ ├── index.js │ └── logger.js ├── test ├── core.js ├── index.js ├── login.js ├── main.js ├── question.js ├── register.js └── user.js ├── util └── db │ └── create.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | server/db/ 3 | server/lib/ 4 | npm-debug.log 5 | client/dist/ 6 | coverage 7 | .vscode 8 | deploy/db 9 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker:latest 2 | services: 3 | - docker:dind 4 | 5 | stages: 6 | - build 7 | - test 8 | - release 9 | - deploy 10 | 11 | variables: 12 | SERVER_TEST_IMAGE: yamalight/bpwjs-server:$CI_BUILD_REF_NAME 13 | CLIENT_TEST_IMAGE: yamalight/bpwjs-client:$CI_BUILD_REF_NAME 14 | SERVER_RELEASE_IMAGE: yamalight/bpwjs-server:latest 15 | CLIENT_RELEASE_IMAGE: yamalight/bpwjs-client:latest 16 | 17 | before_script: 18 | - docker login -u yamalight -p $CI_DOCKERHUB_PASSWORD 19 | 20 | build-server: 21 | stage: build 22 | script: 23 | - docker build --pull -t $SERVER_TEST_IMAGE ./server 24 | - docker push $SERVER_TEST_IMAGE 25 | 26 | build-client: 27 | stage: build 28 | script: 29 | - docker build --pull -t $CLIENT_TEST_IMAGE ./client 30 | - docker push $CLIENT_TEST_IMAGE 31 | 32 | test-server: 33 | stage: test 34 | script: 35 | - docker run -d --name expertsdb rethinkdb 36 | - docker pull $SERVER_TEST_IMAGE 37 | - docker run --link expertsdb:expertsdb -e EXPERTS_DB_URL=expertsdb $SERVER_TEST_IMAGE npm test 38 | 39 | test-client: 40 | stage: test 41 | script: 42 | - docker pull $CLIENT_TEST_IMAGE 43 | - docker run -e TZ=Europe/Berlin $CLIENT_TEST_IMAGE npm test 44 | 45 | release-server: 46 | stage: release 47 | script: 48 | - docker pull $SERVER_TEST_IMAGE 49 | - docker tag $SERVER_TEST_IMAGE $SERVER_RELEASE_IMAGE 50 | - docker push $SERVER_RELEASE_IMAGE 51 | only: 52 | - master 53 | 54 | release-client: 55 | stage: release 56 | script: 57 | - docker pull $CLIENT_TEST_IMAGE 58 | - docker tag $CLIENT_TEST_IMAGE $CLIENT_RELEASE_IMAGE 59 | - docker push $CLIENT_RELEASE_IMAGE 60 | only: 61 | - master 62 | 63 | deploy: 64 | stage: deploy 65 | only: 66 | - master 67 | when: manual 68 | before_script: 69 | # Install ssh-agent, ldap if not already installed 70 | - apk add --update openssh 71 | # Run ssh-agent (inside the build environment) 72 | - eval $(ssh-agent -s) 73 | # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store 74 | - echo "$SSH_PRIVATE_KEY" > ~/id_rsa && chmod 600 ~/id_rsa && ssh-add ~/id_rsa 75 | # For Docker builds disable host key checking. Be aware that by adding that 76 | # you are suspectible to man-in-the-middle attacks. 77 | # WARNING: Use this only with the Docker executor, if you use it with shell 78 | # you will overwrite your user's SSH config. 79 | - mkdir -p ~/.ssh 80 | - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' 81 | script: 82 | - ssh root@codezen.net "cd /root/bpwjs.deploy; make deploy" 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building products with javascript 2 | 3 | > Free open-source course 4 | 5 | [![Build Status](https://gitlab.com/yamalight/building-products-with-js/badges/master/build.svg)](https://gitlab.com/yamalight/building-products-with-js/pipelines) 6 | 7 | This repository contains code and related materials for [Building products with javascript](https://www.youtube.com/playlist?list=PL_gX69xPLi-ljVdNhspjZUlPmBNjRgD2X) course. 8 | 9 | ## Project description 10 | 11 | This is a simple client-server CRUD application that allows users to ask and answer questions. 12 | It uses [express.js](https://expressjs.com/), [passport.js](http://passportjs.org/) with [JWT](https://jwt.io/) and [thinky](https://github.com/neumino/thinky) along with [RethinkDB](https://www.rethinkdb.com/) on a backend; [React](https://facebook.github.io/react/), [Redux](http://redux.js.org/), [RxJS](https://github.com/Reactive-Extensions/RxJS) on front-end. 13 | Backend is tested using [tape](https://github.com/substack/tape) and [supertest](https://github.com/visionmedia/supertest), while front-end uses [jest](https://facebook.github.io/jest/) and [enzyme](https://github.com/airbnb/enzyme). 14 | [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) are used for deployment. 15 | 16 | ## CI/CD 17 | 18 | CI and CD for all the subprojects is done using [Gitlab-CI](https://gitlab.com/yamalight/building-products-with-js). 19 | 20 | ## Useful links 21 | 22 | - [YouTube channel](https://www.youtube.com/c/TimErmilov) with videos covering code 23 | - [Discord chat](https://discord.gg/hnKCXqQ) for questions and live discussions 24 | - [Subreddit](https://www.reddit.com/r/BuildingWithJS/) for discussions 25 | - [Facebook page](https://www.facebook.com/buildingproductswithjs/) with updates on progress 26 | - [My twitter](https://twitter.com/yamalight) with updates on progress (and other stuff) 27 | 28 | ## Course 2: Building Electron.js apps 29 | 30 | My second course on building Electron.js apps can be found [here](https://github.com/yamalight/bpjs-electron) and [here](https://www.youtube.com/playlist?list=PL_gX69xPLi-lBH8I52J-3nEhEQD6_nDs6). 31 | 32 | ## Course 3: Building Data Science apps 33 | 34 | My third course on building data science apps can be found [here](https://github.com/BuildingXwithJS/building-data-science-with-js) and [here](https://www.youtube.com/playlist?list=PL_gX69xPLi-lGe7iRt6DqTZ7PpIrNq8ep). 35 | 36 | ## License 37 | 38 | [MIT](https://opensource.org/licenses/mit-license) 39 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["transform-runtime"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"] 7 | }, 8 | "production": { 9 | "presets": ["react-optimize"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | coverage 4 | dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "jest": true 7 | }, 8 | "globals": { 9 | "shallow": true, 10 | "render": true, 11 | "mount": true, 12 | "API_HOST": true 13 | }, 14 | "rules": { 15 | "object-curly-spacing": ["warn", "never"], 16 | "func-names": "off", 17 | "space-before-function-paren": ["error", "never"], 18 | "max-len": ["error", 120, 4], 19 | "no-unused-vars": ["error", {"argsIgnorePattern": "next"}], 20 | "import/prefer-default-export": "off", 21 | "react/jsx-filename-extension": "off", 22 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}], 23 | "jsx-a11y/anchor-has-content": ["error"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kkarczmarczyk/node-yarn:latest 2 | 3 | # Create app folder 4 | RUN mkdir -p /app 5 | WORKDIR /app 6 | 7 | # Cache npm dependencies 8 | COPY package.json /app/ 9 | COPY yarn.lock /app/ 10 | RUN yarn 11 | 12 | # Copy application files 13 | COPY . /app 14 | 15 | EXPOSE 3000 16 | 17 | CMD ["npm", "start"] 18 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Experts client 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "experts-client", 3 | "version": "0.1.0", 4 | "description": "Experts portal client", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node serve.js", 8 | "test": "jest", 9 | "test:watch": "jest --watch", 10 | "cover": "jest --coverage" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/yamalight/building-products-with-js.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "react-router", 19 | "webpack", 20 | "tutorial", 21 | "front-end" 22 | ], 23 | "author": "Tim Ermilov (http://codezen.net)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/yamalight/building-products-with-js/issues" 27 | }, 28 | "homepage": "https://github.com/yamalight/building-products-with-js#readme", 29 | "jest": { 30 | "setupFiles": [ 31 | "./test/setup.js" 32 | ], 33 | "snapshotSerializers": [ 34 | "/node_modules/enzyme-to-json/serializer" 35 | ], 36 | "moduleNameMapper": { 37 | "^.+\\.(css|scss)$": "identity-obj-proxy" 38 | } 39 | }, 40 | "devDependencies": { 41 | "babel-core": "^6.23.1", 42 | "babel-eslint": "^7.1.1", 43 | "babel-jest": "^19.0.0", 44 | "babel-loader": "^6.3.2", 45 | "babel-plugin-lodash": "^3.2.11", 46 | "babel-plugin-transform-runtime": "^6.23.0", 47 | "babel-preset-es2015": "^6.22.0", 48 | "babel-preset-react": "^6.23.0", 49 | "babel-preset-react-hmre": "^1.1.1", 50 | "babel-preset-react-optimize": "^1.0.1", 51 | "babel-preset-stage-0": "^6.22.0", 52 | "css-loader": "^0.26.2", 53 | "enzyme": "^2.7.1", 54 | "enzyme-to-json": "^1.5.0", 55 | "eslint": "^3.16.1", 56 | "eslint-config-airbnb": "^14.1.0", 57 | "eslint-plugin-import": "^2.2.0", 58 | "eslint-plugin-jsx-a11y": "^4.0.0", 59 | "eslint-plugin-react": "^6.10.0", 60 | "express": "^4.14.1", 61 | "extract-text-webpack-plugin": "^2.0.0", 62 | "file-loader": "^0.10.1", 63 | "identity-obj-proxy": "^3.0.0", 64 | "jest": "^19.0.2", 65 | "json-loader": "^0.5.4", 66 | "lodash-webpack-plugin": "^0.11.2", 67 | "react-addons-test-utils": "^15.4.2", 68 | "redux-mock-store": "^1.2.2", 69 | "style-loader": "^0.13.2", 70 | "url-loader": "^0.5.8", 71 | "webpack": "^2.2.1", 72 | "webpack-dev-middleware": "^1.10.1", 73 | "webpack-hot-middleware": "^2.17.1" 74 | }, 75 | "dependencies": { 76 | "bootstrap": "^3.3.7", 77 | "history": "^4.5.1", 78 | "lodash": "^4.17.4", 79 | "moment": "^2.17.1", 80 | "react": "^15.4.2", 81 | "react-addons-css-transition-group": "^15.4.1", 82 | "react-dom": "^15.4.2", 83 | "react-redux": "^5.0.3", 84 | "react-router": "^3.0.2", 85 | "react-router-redux": "^4.0.8", 86 | "redux": "^3.6.0", 87 | "redux-observable": "^0.13.0", 88 | "rxjs": "^5.2.0" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /client/serve.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | // start webpack 3 | const path = require('path'); 4 | const express = require('express'); 5 | const webpack = require('webpack'); 6 | const webpackMiddleware = require('webpack-dev-middleware'); 7 | const webpackHotMiddleware = require('webpack-hot-middleware'); 8 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 9 | const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); 10 | const config = require('./webpack.config'); 11 | 12 | // create express 13 | const app = express(); 14 | 15 | // get the environment 16 | const isProduction = process.env.NODE_ENV === 'production'; 17 | 18 | // setup plugins 19 | config.plugins = [ 20 | // define plugin for node env 21 | new webpack.DefinePlugin({ 22 | 'process.env': {NODE_ENV: JSON.stringify(process.env.NODE_ENV)}, 23 | API_HOST: JSON.stringify(process.env.API_HOST || 'http://localhost:8080'), 24 | }), 25 | ]; 26 | // if not in prod - setup hot reload 27 | if (!isProduction) { 28 | // hot reload plugin 29 | config.plugins.push(new webpack.HotModuleReplacementPlugin()); 30 | // setup no errors plugin 31 | config.plugins.push(new webpack.NoEmitOnErrorsPlugin()); 32 | } 33 | 34 | // override entry for hot reload 35 | if (!isProduction) { 36 | config.entry = [ 37 | 'webpack-hot-middleware/client', 38 | config.entry, 39 | ]; 40 | } 41 | 42 | // tweak config for production 43 | if (isProduction) { 44 | // set devtool to cheap source map 45 | config.devtool = 'source-map'; 46 | 47 | // extract styles into file 48 | const extractCSS = new ExtractTextPlugin('main.css'); 49 | config.plugins.push(extractCSS); 50 | config.module.rules[0].use = ExtractTextPlugin.extract({ // eslint-disable-line 51 | fallback: 'style-loader', 52 | use: [{ 53 | loader: 'css-loader', 54 | options: { 55 | modules: true, 56 | minimize: true, 57 | }, 58 | }], 59 | }); 60 | config.module.rules[1].use = ExtractTextPlugin.extract({ // eslint-disable-line 61 | fallback: 'style-loader', 62 | use: [{ 63 | loader: 'css-loader', 64 | options: { 65 | minimize: true, 66 | }, 67 | }], 68 | }); 69 | 70 | // add js optimization plugins 71 | config.plugins.push(new webpack.LoaderOptionsPlugin({minimize: true})); 72 | config.plugins.push(new webpack.optimize.UglifyJsPlugin()); 73 | config.plugins.push(new LodashModuleReplacementPlugin()); 74 | } 75 | 76 | // returns a Compiler instance 77 | const compiler = webpack(config); 78 | // stats output config 79 | const statsConf = { 80 | colors: true, 81 | hash: false, 82 | timings: true, 83 | chunks: false, 84 | chunkModules: false, 85 | modules: false, 86 | }; 87 | 88 | // add hot reload middleware if not in production 89 | if (!isProduction) { 90 | app.use(webpackMiddleware(compiler, { 91 | publicPath: config.output.publicPath, 92 | contentBase: 'src', 93 | stats: statsConf, 94 | })); 95 | app.use(webpackHotMiddleware(compiler)); 96 | } else { 97 | compiler.run((err, stats) => { 98 | if (err) { 99 | console.error('Error compiling with webpack:', err); 100 | process.exit(1); 101 | } 102 | 103 | console.log(stats.toString(statsConf)); 104 | }); 105 | } 106 | 107 | // serve statics 108 | app.use(express.static(__dirname)); 109 | // serve index 110 | app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'index.html'))); 111 | // start server 112 | app.listen(3000, (err) => { 113 | if (err) { 114 | console.log(err); 115 | } 116 | console.info('==> Listening on port 3000'); 117 | }); 118 | -------------------------------------------------------------------------------- /client/src/app/__tests__/__snapshots__/app.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`# App 1`] = ` 4 |
7 | test 8 | 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /client/src/app/__tests__/app.test.js: -------------------------------------------------------------------------------- 1 | import App from '../index'; 2 | 3 | test('# App', () => { 4 | const wrapper = shallow( 5 | test 6 | ); 7 | expect(wrapper).toMatchSnapshot(); 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/app/index.js: -------------------------------------------------------------------------------- 1 | // npm packages 2 | import React from 'react'; 3 | 4 | import Footer from '../components/footer'; 5 | 6 | export default ({children}) => ( 7 |
8 | {children} 9 |
10 |
11 | ); 12 | -------------------------------------------------------------------------------- /client/src/components/footer/__tests__/__snapshots__/footer.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`# Footer 1`] = ` 4 |
13 |
16 |
19 | 20 |
21 |
22 |
23 | `; 24 | -------------------------------------------------------------------------------- /client/src/components/footer/__tests__/footer.test.js: -------------------------------------------------------------------------------- 1 | import Footer from '../index'; 2 | 3 | test('# Footer', () => { 4 | const wrapper = shallow(