├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── TODO ├── bin └── web-server.js ├── karma.conf.js ├── package.json ├── src ├── api │ ├── graphql │ │ ├── schema.js │ │ ├── schemas │ │ │ ├── commentSchema.js │ │ │ ├── index.js │ │ │ ├── postSchema.js │ │ │ └── userSchema.js │ │ └── types.js │ ├── models │ │ ├── comment.js │ │ ├── index.js │ │ ├── post.js │ │ └── user.js │ └── thinky.js ├── client.js ├── common │ ├── components │ │ ├── Comment │ │ │ └── Comment.js │ │ ├── CommentBox │ │ │ └── CommentBox.js │ │ ├── LoginForm │ │ │ ├── LoginForm.js │ │ │ └── LoginForm.test.js │ │ ├── Modal │ │ │ └── Modal.js │ │ ├── NavBar │ │ │ ├── NavBar.js │ │ │ └── NavBar.test.js │ │ ├── Post │ │ │ └── Post.js │ │ ├── SignupForm │ │ │ ├── SignupForm.js │ │ │ └── SignupForm.test.js │ │ ├── Thread │ │ │ └── Thread.js │ │ └── index.js │ ├── containers │ │ ├── Comments │ │ │ └── Comments.js │ │ ├── DevTools │ │ │ └── DevTools.js │ │ ├── Header │ │ │ ├── Header.js │ │ │ └── Header.test.js │ │ ├── Html │ │ │ └── Html.js │ │ ├── Login │ │ │ └── Login.js │ │ ├── PostContainer │ │ │ └── PostContainer.js │ │ ├── Signup │ │ │ ├── Signup.js │ │ │ └── validate.js │ │ └── index.js │ ├── decorators │ │ └── checkAuth.js │ ├── redux │ │ ├── actions │ │ │ ├── auth.js │ │ │ ├── auth.test.js │ │ │ └── ui │ │ │ │ ├── forms.js │ │ │ │ ├── forms.test.js │ │ │ │ ├── modals.js │ │ │ │ └── modals.test.js │ │ ├── reducers │ │ │ ├── auth.js │ │ │ ├── auth.test.js │ │ │ ├── index.js │ │ │ └── ui │ │ │ │ ├── forms.js │ │ │ │ ├── forms.test.js │ │ │ │ ├── index.js │ │ │ │ ├── modals.js │ │ │ │ └── modals.test.js │ │ ├── sagas │ │ │ ├── auth.js │ │ │ ├── auth.test.js │ │ │ ├── index.js │ │ │ ├── index.test.js │ │ │ └── ui │ │ │ │ ├── modals.js │ │ │ │ └── modals.test.js │ │ └── store.js │ ├── routes │ │ ├── App.js │ │ ├── Chat │ │ │ └── Chat.js │ │ ├── Home │ │ │ └── Home.js │ │ ├── NotFound │ │ │ └── NotFound.js │ │ └── index.js │ └── utils │ │ ├── ApolloClient.js │ │ └── queries │ │ ├── auth.js │ │ ├── index.js │ │ ├── post.js │ │ └── users.js ├── config.js └── server.js ├── static ├── favicon.ico ├── favicon.png └── images │ └── person.jpg └── webpack ├── dev.config.js ├── prod.config.js ├── tests.webpack.js ├── webpack-dev-server.js └── webpack-isomorphic-tools.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | 4 | "plugins": [ 5 | "transform-runtime", 6 | "add-module-exports", 7 | "transform-decorators-legacy", 8 | "transform-react-display-name" 9 | ], 10 | 11 | "env": { 12 | "development": { 13 | "plugins": [ 14 | "system-import-transformer", 15 | "typecheck", 16 | ["react-transform", { 17 | "transforms": [{ 18 | "transform": "react-transform-catch-errors", 19 | "imports": ["react", "redbox-react"] 20 | } 21 | ] 22 | }] 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | karma.conf.js 3 | tests.webpack.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": "eslint-config-airbnb", 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "react/no-multi-comp": 0, 9 | "react/jsx-filename-extension": 0, 10 | "import/default": 0, 11 | "import/no-duplicates": 0, 12 | "import/named": 0, 13 | "import/namespace": 0, 14 | "import/no-named-as-default": 2, 15 | "comma-dangle": 0, // not sure why airbnb turned this on. gross! 16 | "indent": [2, 2, {"SwitchCase": 1}], 17 | "no-console": 0, 18 | "no-alert": 0, 19 | "new-cap": 0, 20 | "no-unused-vars": 0, 21 | "id-length": 0, 22 | "no-unused-expressions": 0 23 | }, 24 | "plugins": [ 25 | "react", "import", "graphql" 26 | ], 27 | "settings": { 28 | "import/parser": "babel-eslint", 29 | "import/resolve": { 30 | "moduleDirectory": ["node_modules"] 31 | } 32 | }, 33 | "globals": { 34 | "$": true, 35 | "__DEVELOPMENT__": true, 36 | "__CLIENT__": true, 37 | "__SERVER__": true, 38 | "__DISABLE_SSR__": true, 39 | "__DEVTOOLS__": true, 40 | "socket": true, 41 | "webpackIsomorphicTools": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | static/assets 4 | dist/ 5 | *.iml 6 | webpack-assets.json 7 | webpack-stats.json 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simpler stack 2 | ### ***simpler stack*** is a universal javascript full-stack framework built on the best of the latest web technologies like React, Redux, GraphQL, RethinkDB, Apollo, Express, and Webpack 3 | The goal when creating this universal javascript stack was to greatly decrease the size of the boilerplate on a new project, but also include the best of the newest technologies available. It can be a lot to digest at first, but you'll be amazed at how *simple* and quick the development process is when using this stack. 4 | 5 | ![simpler-stack-logos](https://cloud.githubusercontent.com/assets/14098106/17431491/7ecd18ba-5ab7-11e6-902a-253e39aa3e6a.png) 6 | 7 | *special thanks to [mattkrick](https://github.com/mattkrick) for this comparison table, which is borrowed from his [meatier repo](https://github.com/mattkrick/meatier)* 8 | 9 | | Problem | Meteor's solution | simpler's solution | Motivation | 10 | |-------------------|-----------------------------------------------------------------|---------------------------------------------------------------------|---------------------------------------------------------------------| 11 | | Database | [MongoDB](https://www.mongodb.org/) | [RethinkDB](https://www.rethinkdb.com/) | pub/sub, nice web GUI, easy clustering | 12 | | Database schema | [Simple Schema](https://github.com/aldeed/meteor-simple-schema) | [GraphQL](https://github.com/graphql/graphql-js) / [ApolloServer](https://github.com/apollostack/apollo-server) | ApolloServer allows us to use shorthand schema language | 13 | | Client validation | [Simple Schema](https://github.com/aldeed/meteor-simple-schema) | [validator.js](https://github.com/chriso/validator.js) | extremely simple client-side validation | 14 | | Database hooks | [Collections2](https://github.com/aldeed/meteor-collection2) | [GraphQL](https://github.com/graphql/graphql-js) | who isn't using GraphQL anymore? | 15 | | Forms | [AutoForm](https://github.com/aldeed/meteor-autoform) | [redux-form](https://github.com/erikras/redux-form) | automatic form validation, nice form state management | 16 | | Client-side cache | [Minimongo](https://www.meteor.com/mini-databases) | [redux](http://redux.js.org/) | time travel, redo/undo, unidirectional data-flow | 17 | | Socket server | [DDP-server](https://www.meteor.com/ddp) | [socket.io](http://socket.io/) | socket.io v1.0 is out, so that's cool | 18 | | Authentication | Meteor accounts | [JWTs](https://jwt.io) | simple and efficient | 19 | | Auth-transport | [DDP](https://www.meteor.com/ddp) | [ApolloClient](https://github.com/apollostack/apollo-client) (via HTTP fetch) | ApolloClient gives some relay-like features without all the bloat | 20 | | Front-end | [Blaze](https://www.meteor.com/blaze) | [React](https://facebook.github.io/react/) | React is the future of UI development | 21 | | Build system | meteor | [webpack](https://webpack.github.io/) | Webpack to simplify building all the things | 22 | | CSS | magically bundle & serve | [aphrodite](https://github.com/khan/aphrodite) | inline styles, all the features of sass, works with server rendering | 23 | | Optimistic UI | latency compensation | [ApolloClient](https://github.com/apollostack/apollo-client) | optimistic ui with automatic rollback | 24 | | Testing | Velocity (or nothing at all) | [karma](https://github.com/karma-runner/karma) / [mocha](https://github.com/mochajs/mocha) / [chai](https://github.com/chaijs/chai) / [enzyme](https://github.com/airbnb/enzyme) | JQuery-like selectors, automatic test re-run on code change, simple assertions | 25 | | Linting | Your choice | [eslint](https://github.com/eslint/eslint) | the industry standard, live linting with atom plugin 'linter-eslint' | 26 | | Routing | [FlowRouter](https://github.com/kadirahq/flow-router) | [react-router-redux](https://github.com/reactjs/react-router-redux) | ssr capable react router, async routes | 27 | | Server | Node 0.10.41 | Node 5 | ... | | 28 | 29 | ## Installation 30 | - `brew install rethinkdb` 31 | - `rethinkdb` (in second terminal window) 32 | - `git clone` this repo 33 | - `cd simpler-stack` 34 | - `npm install` 35 | 36 | ## Development 37 | - `npm run dev` (hot reloaded dev server) 38 | 39 | ## Production 40 | - `npm run build` 41 | - `npm run start` 42 | 43 | ## Similar Projects 44 | - https://github.com/erikras/react-redux-universal-hot-example (Huge motivation for the architecture of this stack, this was the original boilerplate forked to create this stack) 45 | - https://github.com/mattkrick/meatier (Another big motivation, but over complex. I also borrowed the table above from this repo) 46 | - https://github.com/kriasoft/react-starter-kit 47 | - https://github.com/GordyD/3ree 48 | 49 | ## Contributing 50 | - This project is massively outdated and could do with an overhaul, but unfortunately I am too busy with other projects at this time to update it myself. I am willing to review/accept any pull requests! 51 | 52 | ## License 53 | MIT 54 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO: 2 | ☐ Move tests to __TESTS__ folder 3 | ☐ Explore "afterware" for cleaning up GQL errors and remove user exposure 4 | ☐ migrate to stardust 5 | ☐ Async server-validation for forms on blur example 6 | ✔ this library 'validator.js' only validates strings bug @done(2016-08-06 18:23) 7 | ✔ opt-in store persistence @done(2016-08-05 22:00) 8 | ✔ Implement updateQueries @done(2016-08-05 15:32) 9 | ✔ Implement optimistic UI https://medium.com/apollo-stack/mutations-and-optimistic-ui-in-apollo-client-517eacee8fb0#.9gvxcznj4 @done(2016-08-05 22:00) 10 | ✔ Remove all jQuery dependencies @done(2016-08-06 18:23) 11 | 12 | TESTING: 13 | Add unit testing 14 | ☐ API Server 15 | ✔ Redux @done(2016-08-02 23:49) 16 | ☐ Containers: 17 | ☐ we can possibly use a global describe in tests.webpack.js 18 | to preload an ApolloProvider to mount all other containers to. 19 | ✔ Components @done(2016-08-01 10:12) 20 | ✔ Figure out a way to move redux form out of components to containers @done(2016-08-01 08:52) 21 | ✔ Move modal back to header @done(2016-08-01 08:45) 22 | ✔ Email/password error props @done(2016-08-01 08:52) 23 | ✔ Hopefully this makes components stateless and will allow us to shallow render @done(2016-08-01 09:19) 24 | -------------------------------------------------------------------------------- /bin/web-server.js: -------------------------------------------------------------------------------- 1 | // enable runtime transpilation to use ES6/7 in node 2 | 3 | const fs = require('fs'); 4 | 5 | const babelrc = fs.readFileSync('./.babelrc'); 6 | let config; 7 | 8 | try { 9 | config = JSON.parse(babelrc); 10 | } catch (err) { 11 | console.error('==> ERROR: Error parsing your .babelrc.'); 12 | console.error(err); 13 | } 14 | 15 | require('babel-register')(config); 16 | require('babel-polyfill'); 17 | 18 | const path = require('path'); 19 | const rootDir = path.resolve(__dirname, '..'); 20 | /** 21 | * Define isomorphic constants. 22 | */ 23 | global.__CLIENT__ = false; 24 | global.__SERVER__ = true; 25 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING 26 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; 27 | 28 | if (__DEVELOPMENT__) { 29 | if (!require('piping')({ hook: true, ignore: /(\/\.|~$|\.json$)/i })) { 30 | return; 31 | } 32 | } 33 | 34 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 35 | const WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 36 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools')) 37 | .development(__DEVELOPMENT__) 38 | .server(rootDir, () => { 39 | require('../src/server'); 40 | }); 41 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | browsers: ['jsdom'], 7 | 8 | singleRun: false, 9 | 10 | frameworks: [ 'mocha' ], 11 | 12 | files: [ 13 | './webpack/tests.webpack.js' 14 | ], 15 | 16 | preprocessors: { 17 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 18 | }, 19 | 20 | reporters: [ 'webpack-error' ], 21 | 22 | plugins: [ 23 | require("karma-webpack"), 24 | require("karma-mocha"), 25 | require("karma-webpack-error-reporter"), 26 | require("karma-jsdom-launcher"), 27 | require("karma-sourcemap-loader") 28 | ], 29 | 30 | mochaReporter: { 31 | showDiff: true 32 | }, 33 | 34 | webpack: { 35 | devtool: 'inline-source-map', 36 | module: { 37 | loaders: [ 38 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} }, 39 | { test: /\.js$/, exclude: /node_modules/, loaders: ['babel']}, 40 | { test: /\.json$/, loader: 'json-loader' } 41 | ] 42 | }, 43 | externals: { 44 | 'cheerio': 'window', 45 | 'react/addons': true, 46 | 'react/lib/ExecutionEnvironment': true, 47 | 'react/lib/ReactContext': true 48 | }, 49 | node: { 50 | net: 'empty', 51 | tls: 'empty', 52 | dns: 'empty' 53 | }, 54 | resolve: { 55 | modulesDirectories: [ 56 | 'src', 57 | 'node_modules' 58 | ], 59 | extensions: ['', '.json', '.js'] 60 | }, 61 | plugins: [ 62 | new webpack.IgnorePlugin(/\.json$/), 63 | new webpack.NoErrorsPlugin(), 64 | new webpack.DefinePlugin({ 65 | __CLIENT__: true, 66 | __SERVER__: false, 67 | __DEVELOPMENT__: true, 68 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE 69 | }) 70 | ] 71 | }, 72 | 73 | webpackServer: { 74 | noInfo: true 75 | } 76 | 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-marketplace", 3 | "description": "A marketplace for UI/UX designers and developers", 4 | "author": "Adam King (http://github.com/adamjking3)", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "repository": { 8 | "type": "git", 9 | "url": "" 10 | }, 11 | "homepage": "https://uibuffs.com/", 12 | "main": "bin/web-server.js", 13 | "scripts": { 14 | "start": "concurrently --kill-others \"npm run start-prod\"", 15 | "start-prod": "NODE_ENV=production node ./bin/web-server.js", 16 | "build": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js", 17 | "postinstall": "npm run build", 18 | "lint": "eslint -c .eslintrc src", 19 | "start-dev": "NODE_ENV=development node ./bin/web-server.js", 20 | "watch-client": "UV_THREADPOOL_SIZE=100 node webpack/webpack-dev-server.js", 21 | "dev": "concurrently --kill-others \"npm run watch-client\" \"npm run start-dev\"", 22 | "test": "karma start" 23 | }, 24 | "dependencies": { 25 | "@sketchpixy/rubix": "^1.1.3", 26 | "aphrodite": "^0.5.0", 27 | "apollo-client": "^0.4.11", 28 | "apollo-server": "^0.1.5", 29 | "babel-core": "^6.13.1", 30 | "babel-loader": "^6.2.1", 31 | "babel-plugin-add-module-exports": "^0.2.1", 32 | "babel-plugin-react-transform": "^2.0.0", 33 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 34 | "babel-plugin-transform-react-display-name": "^6.3.13", 35 | "babel-plugin-transform-runtime": "^6.3.13", 36 | "babel-polyfill": "^6.13.0", 37 | "babel-preset-es2015": "^6.13.1", 38 | "babel-preset-react": "^6.3.13", 39 | "babel-preset-stage-0": "^6.3.13", 40 | "babel-register": "^6.3.13", 41 | "bcryptjs": "^2.3.0", 42 | "better-npm-run": "0.0.10", 43 | "classnames": "^2.2.5", 44 | "compression": "^1.6.0", 45 | "concurrently": "^2.2.0", 46 | "express": "^4.13.3", 47 | "extract-text-webpack-plugin": "^1.0.1", 48 | "file-loader": "^0.9.0", 49 | "graphql": "^0.6.0", 50 | "graphql-errors": "^2.1.0", 51 | "graphql-tools": "^0.6.2", 52 | "http-proxy": "^1.12.0", 53 | "intl-messageformat": "^1.3.0", 54 | "javascript-time-ago": "^0.4.3", 55 | "joi": "^9.0.1", 56 | "jsonwebtoken": "^7.1.6", 57 | "lodash": "^4.14.1", 58 | "mocha": "^3.0.1", 59 | "node-sass": "^3.8.0", 60 | "node-uuid": "^1.4.7", 61 | "pretty-error": "^2.0.0", 62 | "react": "^15.1.0", 63 | "react-apollo": "^0.3.19", 64 | "react-dom": "^15.1.0", 65 | "react-dom-stream": "^0.5.1", 66 | "react-helmet": "^3.1.0", 67 | "react-redux": "^4.0.0", 68 | "react-router": "^2.6.1", 69 | "react-router-redux": "^4.0.0", 70 | "react-semantify": "^0.5.1", 71 | "react-time": "^4.2.0", 72 | "react-time-ago": "^0.2.2", 73 | "react-timeago": "^3.1.2", 74 | "redux": "^3.0.4", 75 | "redux-form": "^5.3.2", 76 | "redux-logger": "^2.6.1", 77 | "redux-persist": "^3.2.2", 78 | "redux-saga": "^0.11.0", 79 | "reselect": "^2.5.3", 80 | "rethinkdb": "^2.3.2", 81 | "scroll-behavior": "^0.7.0", 82 | "serialize-javascript": "^1.1.2", 83 | "serve-favicon": "^2.3.0", 84 | "socket.io": "^1.3.7", 85 | "socket.io-client": "^1.3.7", 86 | "stardust": "^0.30.0", 87 | "thinky": "^2.3.2", 88 | "url-loader": "^0.5.7", 89 | "validator": "^5.4.0", 90 | "warning": "^3.0.0", 91 | "webpack-isomorphic-tools": "^2.2.18" 92 | }, 93 | "devDependencies": { 94 | "babel-eslint": "^6.1.2", 95 | "babel-plugin-system-import-transformer": "^2.2.1", 96 | "babel-plugin-typecheck": "^3.9.0", 97 | "better-npm-run": "0.0.8", 98 | "chai": "^3.3.0", 99 | "chai-enzyme": "^0.5.0", 100 | "clean-webpack-plugin": "^0.1.6", 101 | "enzyme": "^2.4.1", 102 | "eslint": "^1.10.3", 103 | "eslint-config-airbnb": "^0.1.1", 104 | "eslint-loader": "^1.0.0", 105 | "eslint-plugin-graphql": "^0.2.4", 106 | "eslint-plugin-import": "^0.8.1", 107 | "eslint-plugin-react": "^3.16.1", 108 | "extract-text-webpack-plugin": "^0.9.1", 109 | "json-loader": "^0.5.4", 110 | "karma": "^1.1.2", 111 | "karma-chrome-launcher": "^1.0.1", 112 | "karma-cli": "^1.0.1", 113 | "karma-jsdom-launcher": "^3.0.0", 114 | "karma-mocha": "^1.1.1", 115 | "karma-mocha-reporter": "^2.1.0", 116 | "karma-sourcemap-loader": "^0.3.7", 117 | "karma-webpack": "^1.7.0", 118 | "karma-webpack-error-reporter": "0.0.2", 119 | "mocha": "^3.0.0", 120 | "phantomjs": "^2.1.7", 121 | "phantomjs-polyfill": "0.0.2", 122 | "piping": "^0.3.0", 123 | "react-addons-test-utils": "^15.1.0", 124 | "react-transform-catch-errors": "^1.0.0", 125 | "react-transform-hmr": "^1.0.1", 126 | "redbox-react": "^1.1.1", 127 | "redux-devtools": "^3.3.1", 128 | "redux-devtools-dock-monitor": "^1.1.1", 129 | "redux-devtools-log-monitor": "^1.0.11", 130 | "strip-loader": "^0.1.0", 131 | "webpack": "^1.12.9", 132 | "webpack-dev-middleware": "^1.4.0", 133 | "webpack-hot-middleware": "^2.5.0" 134 | }, 135 | "engines": { 136 | "node": "5.6.0" 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/api/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import { 2 | userTypes, userQueries, userMutations, userResolvers, 3 | postTypes, postQueries, postMutations, postResolvers, 4 | commentTypes, commentQueries, commentMutations, commentResolvers, 5 | } from './schemas'; 6 | import { EmailScalar, PasswordScalar, DateScalar } from './types'; 7 | 8 | export const schema = [` 9 | scalar Email 10 | scalar Password 11 | scalar Date 12 | 13 | ${userTypes} 14 | ${postTypes} 15 | ${commentTypes} 16 | 17 | type RootQuery { 18 | ${userQueries} 19 | ${postQueries} 20 | ${commentQueries} 21 | } 22 | 23 | type RootMutation { 24 | ${userMutations} 25 | ${postMutations} 26 | ${commentMutations} 27 | } 28 | 29 | schema { 30 | query: RootQuery 31 | mutation: RootMutation 32 | } 33 | `]; 34 | 35 | export const resolvers = { 36 | Email: EmailScalar, 37 | Password: PasswordScalar, 38 | Date: DateScalar, 39 | RootQuery: { 40 | ...userResolvers.Query, 41 | ...postResolvers.Query, 42 | ...commentResolvers.Query, 43 | }, 44 | RootMutation: { 45 | ...userResolvers.Mutation, 46 | ...postResolvers.Mutation, 47 | ...commentResolvers.Mutation, 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/api/graphql/schemas/commentSchema.js: -------------------------------------------------------------------------------- 1 | import { Comment, Post } from '../../models'; 2 | 3 | export const commentTypes = ` 4 | type Comment { 5 | id: String! 6 | description: String! 7 | author: String! 8 | createdAt: Date! 9 | } 10 | `; 11 | 12 | export const commentQueries = ` 13 | comment(id: String!): Comment 14 | commentsForPost(id: String!): [Comment] 15 | commentsByUser(id: String!): [Comment] 16 | `; 17 | 18 | export const commentMutations = ` 19 | addCommentToPost(postId: String!, description: String!, author: String!): Post 20 | `; 21 | 22 | export const commentResolvers = { 23 | Query: { 24 | comment: async (_, { id }) => await Comment.get(id).run(), 25 | commentsForPost: async (_, { id }) => await Comment.filter({ postId: id }).run(), 26 | commentsByUser: async (_, { id }) => await Comment.filter({ authorId: id }).run() 27 | }, 28 | Mutation: { 29 | addCommentToPost: async (_, { postId, description, author }) => { 30 | if (!description) { 31 | throw new Error('Cannot create a comment without a description'); 32 | } 33 | if (!author) { 34 | throw new Error('Cannot create a comment without an author id'); 35 | } 36 | const post = await Post.get(postId).run(); 37 | const comment = new Comment({ description, author, post }); 38 | await comment.saveAll({ post: true }); 39 | console.log(comment); 40 | return comment; 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/api/graphql/schemas/index.js: -------------------------------------------------------------------------------- 1 | export { userTypes, userQueries, userMutations, userResolvers } from './userSchema'; 2 | export { postTypes, postQueries, postMutations, postResolvers } from './postSchema'; 3 | export { commentTypes, commentQueries, commentMutations, commentResolvers } from './commentSchema'; 4 | -------------------------------------------------------------------------------- /src/api/graphql/schemas/postSchema.js: -------------------------------------------------------------------------------- 1 | import { Post, User, Comment } from '../../models'; 2 | 3 | export const postTypes = ` 4 | type Post { 5 | id: String! 6 | title: String! 7 | description: String! 8 | author: String! 9 | createdAt: Date! 10 | } 11 | `; 12 | 13 | export const postQueries = ` 14 | post(id: String!): Post 15 | allPosts: [Post] 16 | postsByUser(id: String!): [Post] 17 | `; 18 | 19 | export const postMutations = ` 20 | createPost(title: String!, description: String!, author: String!): Post 21 | `; 22 | 23 | export const postResolvers = { 24 | Query: { 25 | post: async (_, { id }) => await Post.get(id).run(), 26 | allPosts: async () => await Post.run(), 27 | postsByUser: async (_, { id }) => await Post.filter({ authorId: id }).run() 28 | }, 29 | Mutation: { 30 | createPost: async (_, { title, description, author }) => { 31 | if (!title) { 32 | throw new Error('Cannot create a post without a title'); 33 | } 34 | if (!description) { 35 | throw new Error('Cannot create a post without a description'); 36 | } 37 | if (!author) { 38 | throw new Error('Cannot create a post without an author id'); 39 | } 40 | const post = new Post({ title, description, author }); 41 | await post.saveAll(); 42 | return post; 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/api/graphql/schemas/userSchema.js: -------------------------------------------------------------------------------- 1 | import { User } from '../../models'; 2 | 3 | export const userTypes = ` 4 | type User { 5 | id: String! 6 | name: String! 7 | email: Email! 8 | authToken: String 9 | } 10 | `; 11 | 12 | export const userQueries = ` 13 | user(id: String!): User 14 | users: [User] 15 | currentUser: User 16 | loginUser(email: Email!, password: Password!): User 17 | `; 18 | 19 | export const userMutations = ` 20 | createUser(name: String!, email: Email!, password: Password!): User 21 | `; 22 | 23 | export const userResolvers = { 24 | Query: { 25 | user: async (root, { id }) => await User.get(id).run(), 26 | users: async () => await User.run(), 27 | currentUser: (_, __, context) => context.user, 28 | loginUser: async (root, { email, password }) => { 29 | const [ user ] = await User.filter({ email }).run(); 30 | if (!user) { 31 | throw new Error('No user with that email address exists.'); 32 | } else if (! await user.validatePassword(password)) { 33 | throw new Error('Invalid email or password.'); 34 | } 35 | const authedUser = { 36 | ...user, 37 | authToken: user.signJwt() 38 | }; 39 | return authedUser; 40 | }, 41 | }, 42 | Mutation: { 43 | createUser: async (root, { name, email, password }) => { 44 | const [ exists ] = await User.filter({ email }).run(); 45 | if (exists) { 46 | throw new Error('That email address is already in use.'); 47 | } 48 | const user = new User({ name, email }); 49 | await user.setPassword(password); 50 | const authedUser = { 51 | ...user, 52 | authToken: user.signJwt() 53 | }; 54 | return authedUser; 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/api/graphql/types.js: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql/error'; 2 | import { Kind } from 'graphql/language'; 3 | import validator from 'validator'; 4 | 5 | export const DateScalar = { 6 | __serialize: value => String(value), 7 | __parseValue: value => String(value), 8 | __parseLiteral: ast => { 9 | if (ast.kind !== Kind.STRING) { 10 | throw new GraphQLError(`Query error: Date is not a string, it is a: ${ast.kind}`, [ast]); 11 | } 12 | const result = new Date(ast.value); 13 | if (isNaN(result.getTime())) { 14 | throw new GraphQLError('Query error: Invalid date', [ast]); 15 | } 16 | if (ast.value !== result.toJSON()) { 17 | throw new GraphQLError('Query error: Invalid date format, only accepts: YYYY-MM-DDTHH:MM:SS.SSSZ', [ast]); 18 | } 19 | return result; 20 | } 21 | }; 22 | 23 | export const EmailScalar = { 24 | __serialize: value => validator.normalizeEmail(value), 25 | __parseValue: value => validator.normalizeEmail(value), 26 | __parseLiteral: ast => { 27 | if (ast.kind !== Kind.STRING) { 28 | throw new GraphQLError(`Query error: Email is not a string, it is a: ${ast.kind}`, [ast]); 29 | } 30 | if (!validator.isEmail(ast.value)) { 31 | throw new GraphQLError('Query error: Invalid email', [ast]); 32 | } 33 | if (ast.value.length < 4) { 34 | throw new GraphQLError(`Query error: Email is too short. The minimum length is 4.`, [ast]); 35 | } 36 | if (ast.value.length > 255) { 37 | throw new GraphQLError(`Query error: Email is too long. The maximum length is 255.`, [ast]); 38 | } 39 | return validator.normalizeEmail(ast.value); 40 | } 41 | }; 42 | 43 | export const PasswordScalar = { 44 | __serialize: value => String(value), 45 | __parseValue: value => String(value), 46 | __parseLiteral: ast => { 47 | if (ast.kind !== Kind.STRING) { 48 | throw new GraphQLError(`Query error: Password is not a string, it is a: ${ast.kind}`, [ast]); 49 | } 50 | if (ast.value.length < 6) { 51 | throw new GraphQLError(`Query error: Password is too long. The minimum length is 6.`, [ast]); 52 | } 53 | if (ast.value.length > 64) { 54 | throw new GraphQLError(`Query error: Password is too long. The maximum length is 64.`, [ast]); 55 | } 56 | return String(ast.value); 57 | } 58 | }; 59 | 60 | export const URLScalar = { 61 | __serialize: value => String(value), 62 | __parseValue: value => String(value), 63 | __parseLiteral: ast => { 64 | if (!validator.isURL(ast.value)) { 65 | throw new GraphQLError('Query error: Invalid URL', [ast]); 66 | } 67 | if (ast.kind !== Kind.STRING) { 68 | throw new GraphQLError(`Query error: URL is not a string, it is a: ${ast.kind}`, [ast]); 69 | } 70 | if (ast.value.length < 1) { 71 | throw new GraphQLError(`Query error: URL is too short. The minimum length is 1.`, [ast]); 72 | } 73 | if (ast.value.length > 2083) { 74 | throw new GraphQLError(`Query error: URL is too long. The maximum length is 2083.`, [ast]); 75 | } 76 | return String(ast.value); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/api/models/comment.js: -------------------------------------------------------------------------------- 1 | import thinky from '../thinky'; 2 | 3 | const { type } = thinky; 4 | const Comment = thinky.createModel('comment', { 5 | id: type.string(), 6 | author: type.string(), 7 | description: type.string().max(2048), 8 | createdAt: type.date().default(Date.now()), 9 | authorId: type.string(), 10 | }); 11 | 12 | export default Comment; 13 | -------------------------------------------------------------------------------- /src/api/models/index.js: -------------------------------------------------------------------------------- 1 | export Comment from './comment'; 2 | export Post from './post'; 3 | export User from './user'; 4 | -------------------------------------------------------------------------------- /src/api/models/post.js: -------------------------------------------------------------------------------- 1 | import thinky from '../thinky'; 2 | import Comment from './comment'; 3 | 4 | const { type } = thinky; 5 | const Post = thinky.createModel('post', { 6 | id: type.string(), 7 | author: type.string(), 8 | title: type.string().max(255), 9 | description: type.string().max(2048), 10 | createdAt: type.date().default(Date.now()), 11 | comments: type.array().default([]), 12 | authorId: type.string() 13 | }); 14 | 15 | Post.hasMany(Comment, 'comments', 'id', 'postId'); 16 | Comment.belongsTo(Post, 'post', 'postId', 'id'); 17 | 18 | export default Post; 19 | -------------------------------------------------------------------------------- /src/api/models/user.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import jwt from 'jsonwebtoken'; 3 | 4 | import thinky from '../thinky'; 5 | import { secretKey, isProduction } from '../../config'; 6 | import Post from './post'; 7 | import Comment from './comment'; 8 | 9 | const bcrypt = Promise.promisifyAll(require('bcryptjs')); 10 | const verifyJwt = Promise.promisify(jwt.verify); 11 | const { type } = thinky; 12 | const User = thinky.createModel('user', { 13 | id: type.string(), 14 | name: type.string(), 15 | email: type.string(), 16 | salt: type.string(), 17 | hash: type.string(), 18 | createdAt: type.date().default(Date.now()), 19 | posts: type.array().default([]), 20 | }); 21 | 22 | User.hasMany(Post, 'posts', 'id', 'authorId'); 23 | Post.belongsTo(User, 'user', 'authorId', 'id'); 24 | 25 | Comment.hasOne(User, 'user', 'id', 'authorId'); 26 | User.belongsTo(Comment, 'comment', 'authorId', 'id'); 27 | 28 | User.define('signJwt', function signJwt() { 29 | return jwt.sign({ id: this.id }, secretKey, { expiresIn: '7d' }); 30 | }); 31 | 32 | User.defineStatic('fromToken', async function fromToken(token) { 33 | if (!token) { throw new Error('No token was given.'); } 34 | const { id } = await verifyJwt(token, secretKey); 35 | return await User.get(id); 36 | }); 37 | 38 | User.define('validatePassword', async function validatePassword(password) { 39 | if (!password) { throw new Error('No password was given.'); } 40 | return await bcrypt.compareAsync(password, this.hash); 41 | }); 42 | 43 | User.define('setPassword', async function setPassword(password) { 44 | if (!password) { throw new Error('No password was given.'); } 45 | 46 | const rounds = isProduction ? 12 : 10; 47 | 48 | this.salt = await bcrypt.genSaltAsync(rounds); 49 | this.hash = await bcrypt.hashAsync(password, this.salt); 50 | 51 | await this.save(); 52 | }); 53 | 54 | export default User; 55 | -------------------------------------------------------------------------------- /src/api/thinky.js: -------------------------------------------------------------------------------- 1 | import thinky from 'thinky'; 2 | import config from '../config'; 3 | 4 | const Thinky = thinky(config.rethinkDB); 5 | 6 | export default Thinky; 7 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER. 3 | */ 4 | import 'babel-polyfill'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import io from 'socket.io-client'; 8 | import { ApolloProvider } from 'react-apollo'; 9 | import { Router, browserHistory as routerHistory } from 'react-router'; 10 | import { syncHistoryWithStore } from 'react-router-redux'; 11 | import { StyleSheet } from 'aphrodite'; 12 | import withScroll from 'scroll-behavior'; 13 | 14 | import createStore from './common/redux/store'; 15 | import rootSaga from './common/redux/sagas'; 16 | import getRoutes from './common/routes'; 17 | import ApolloClient from './common/utils/ApolloClient'; 18 | 19 | const dest = document.getElementById('content'); 20 | 21 | const client = ApolloClient(); 22 | const browserHistory = withScroll(routerHistory); 23 | const store = createStore(browserHistory, client, window.__data); 24 | const history = syncHistoryWithStore(browserHistory, store); 25 | const routes = getRoutes(store); 26 | 27 | store.runSaga(rootSaga); 28 | 29 | global.socket = () => { 30 | const socket = io('', { path: '/ws' }); 31 | socket.on('msg', (data) => { 32 | console.log(data); 33 | socket.emit('my other event', { my: 'data from client' }); 34 | }); 35 | return socket; 36 | }; 37 | 38 | if (process.env.NODE_ENV !== 'production') { 39 | window.React = React; // enable debugger 40 | } 41 | 42 | StyleSheet.rehydrate(window.renderedClassNames); 43 | 44 | ReactDOM.render( 45 | 46 | 47 | {routes} 48 | 49 | , 50 | dest 51 | ); 52 | -------------------------------------------------------------------------------- /src/common/components/Comment/Comment.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { StyleSheet, css } from 'aphrodite'; 3 | import { Image, Button, Icon, Divider, Items, Segment, Header } from 'react-semantify'; 4 | import TimeAgo from 'react-timeago'; 5 | 6 | const styles = StyleSheet.create({ 7 | description: { 8 | fontSize: '16px', 9 | paddingBottom: '20px', 10 | } 11 | }); 12 | 13 | export default class Comment extends Component { 14 | static propTypes = { 15 | author: PropTypes.string, 16 | createdAt: PropTypes.string, 17 | description: PropTypes.string, 18 | }; 19 | 20 | static defaultProps = { 21 | author: 'that other guy', 22 | description: `Holy cow! That is really cool!`, 23 | createdAt: 'Mon Aug 08 2016 15:23:27 GMT-0600 (MDT)' 24 | }; 25 | 26 | render() { 27 | const { author, description, createdAt } = this.props; 28 | return ( 29 | 30 | 31 |
32 | 33 |
34 |
35 | {author && author} • {createdAt && } 36 |
37 | 38 |
39 |

40 | {description && description} 41 |

42 |
43 |
44 | 47 | 50 | 53 | 56 |
57 |
58 |
59 |
60 |
61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/common/components/CommentBox/CommentBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { StyleSheet, css } from 'aphrodite'; 3 | import { Button, Icon, Segment, Form } from 'react-semantify'; 4 | 5 | const styles = StyleSheet.create({ 6 | }); 7 | 8 | export default class CommentBox extends Component { 9 | static propTypes = { 10 | classNames: PropTypes.string 11 | }; 12 | 13 | render() { 14 | return ( 15 |
16 |
17 |