Skip to content
This repository was archived by the owner on Jun 28, 2021. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Next Next commit
Rewrite on top
  • Loading branch information
mmahalwy committed Jan 5, 2016
commit 0f4edcf449681b6f280dfa71e51065f745617bb5
Binary file renamed static/fonts/.DS_Store → .DS_Store
Binary file not shown.
46 changes: 21 additions & 25 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,39 +1,35 @@
FROM ubuntu

ENV NODE_ENV production

RUN apt-get -y update && apt-get -y install \
nodejs npm supervisor nodejs-legacy ssh rsync

# logrotate
RUN apt-get -y install logrotate
COPY docker/supervisord.conf /etc/supervisor/supervisord.conf
COPY docker/pm2.logrotate.conf /etc/logrotate.d/pm2
RUN cp /etc/cron.daily/logrotate /etc/cron.hourly
FROM node:5.1.1

# cache npm install when package.json hasn't changed
WORKDIR /tmp
ADD package.json package.json
RUN npm install
RUN npm install -g pm2

RUN mkdir /quran
RUN cp -a /tmp/node_modules /quran
RUN mkdir /sparrow
RUN cp -a /tmp/node_modules /sparrow

WORKDIR /quran
ADD . /quran/
WORKDIR /sparrow
ADD . /sparrow/
ENV NODE_ENV production
ENV API_URL http://marketplace.peek.com
ENV PIRATE_URL http://www.peek.com
RUN npm run build

# ssh keys
WORKDIR /root
RUN mv /quran/.ssh /root/

# upload js and css
WORKDIR /quran/build
RUN rsync --update --progress -raz main* [email protected]:zones/assets/
WORKDIR /sparrow/build
# UPLOAD TO S3!

# go back to /quran
WORKDIR /quran
# go back to /sparrow
WORKDIR /sparrow

ENV NODE_ENV production
ENV NODE_PATH "./src"
ENV HOST 127.0.0.1
ENV PORT 8000
ENV API_URL http://marketplace.peek.com
ENV PIRATE_URL http://www.peek.com
ENV DISABLE_SSR false

EXPOSE 8000
CMD ["supervisord", "--nodaemon", "-c", "/etc/supervisor/supervisord.conf"]
CMD ["pm2", "start", "./bin/server.js", "--no-daemon", "-i", "0"]
168 changes: 139 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,150 @@
## Quran.com
This is the project soon to be the Quran.com facing site. This is built in
Reactjs + Flux (Fluxible by Yahoo) + Expressjs + Webpack. It is isomorphic (javascript shared
between both the server and the client) for SEO reasons.
# Quran.com

#### Getting started
Simply clone this repo, then run `npm install` to install all the required node_modules.
From there, you are ready to go! To start the app, run `npm run watch` which will
run both the server and the client (webpack) to compile upon edits. In fact,
hot-module has been added that components will update without the need to refresh
the page.
## About

#### Tests
Run `npm run test:watch` to run the tests locally and watching. Otherwise use `npm run test` for CI level tests.
This is a starter boilerplate app I've put together using the following technologies:

We also have nightwatch function tests. You can install nightwatch globally and can run tests like this:
* ~~Isomorphic~~ [Universal](https://medium.com/@mjackson/universal-javascript-4761051b7ae9) rendering
* Both client and server make calls to load data from separate API server
* [React](https://github.com/facebook/react)
* [React Router](https://github.com/rackt/react-router)
* [Express](http://expressjs.com)
* [Babel](http://babeljs.io) for ES6 and ES7 magic
* [Webpack](http://webpack.github.io) for bundling
* [Webpack Dev Middleware](http://webpack.github.io/docs/webpack-dev-middleware.html)
* [Webpack Hot Middleware](https://github.com/glenjamin/webpack-hot-middleware)
* [Redux](https://github.com/rackt/redux)'s futuristic [Flux](https://facebook.github.io/react/blog/2014/05/06/flux.html) implementation
* [Redux Dev Tools](https://github.com/gaearon/redux-devtools) for next generation DX (developer experience). Watch [Dan Abramov's talk](https://www.youtube.com/watch?v=xsSnOQynTHs).
* [Redux Router](https://github.com/rackt/redux-router) Keep your router state in your Redux store
* [ESLint](http://eslint.org) to maintain a consistent code style
* [redux-form](https://github.com/erikras/redux-form) to manage form state in Redux
* [lru-memoize](https://github.com/erikras/lru-memoize) to speed up form validation
* [multireducer](https://github.com/erikras/multireducer) to combine single reducers into one key-based reducer
* [style-loader](https://github.com/webpack/style-loader), [sass-loader](https://github.com/jtangelder/sass-loader) and [less-loader](https://github.com/webpack/less-loader) to allow import of stylesheets in plain css, sass and less,
* [bootstrap-sass-loader](https://github.com/shakacode/bootstrap-sass-loader) and [font-awesome-webpack](https://github.com/gowravshekar/font-awesome-webpack) to customize Bootstrap and FontAwesome
* [react-document-meta](https://github.com/kodyl/react-document-meta) to manage title and meta tag information on both server and client
* [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) to allow require() work for statics both on client and server
* [mocha](https://mochajs.org/) to allow writing unit tests for the project.

I cobbled this together from a wide variety of similar "starter" repositories. As I post this in June 2015, all of these libraries are right at the bleeding edge of web development. They may fall out of fashion as quickly as they have come into it, but I personally believe that this stack is the future of web development and will survive for several years. I'm building my new projects like this, and I recommend that you do, too.

## Installation

```bash
npm install
```
nightwatch --test tests/functional/specs/Index_spec.js

## Running Dev Server

```bash
npm run dev
```

#### Backend
Current at: https://github.com/quran/quran-api-rails
DB is private, message me for acceess.
The first time it may take a little while to generate the first `webpack-assets.json` and complain with a few dozen `[webpack-isomorphic-tools] (waiting for the first Webpack build to finish)` printouts, but be patient. Give it 30 seconds.

### Using Redux DevTools

#### How to contribute
Fork this repo, then create a PR for specific fixes, improvements, etc. We trust that
you will not steal this, this is at the end of the day for the sake of Allah and we
all have good intentions while working with this project. But I must stress, stealing
this is unacceptable.
In development, Redux Devtools are enabled by default. You can toggle visibility and move the dock around using the following keyboard shortcuts:

#### Design
We currently use InvisionApp. Again, contact me if you'd like access to it.
- <kbd>Ctrl+H</kbd> Toggle DevTools Dock
- <kbd>Ctrl+Q</kbd> Move Dock Position
- see [redux-devtools-dock-monitor](https://github.com/gaearon/redux-devtools-dock-monitor) for more detail information.

#### Making sure main.js is small
Follow: https://www.npmjs.com/package/webpack-bundle-size-analyzer
## Building and Running Production Server

```bash
npm run build
npm run start
```

If you want to mimic what production is like, I encourage you to install pm2
```bash
npm install pm2 -g
```
env NODE_ENV=development webpack --json > bundle-stats.json
subl bundle-stats.json #so that you can the output
analyze-bundle-size bundle-stats.json

Then run
```bash
env NODE_PATH="./src" NODE_ENV=production pm2 start ./bin/server.js
```
Navigate to localhost:4600 if you're on local. Prod should be 8000.

## Explanation

What initially gets run is `bin/server.js`, which does little more than enable ES6 and ES7 awesomeness in the
server-side node code. It then initiates `server.js`. In `server.js` we proxy any requests to `/api/*` to the
[API server](#api-server), running at `localhost:3030`. All the data fetching calls from the client go to `/api/*`.
Aside from serving the favicon and static content from `/static`, the only thing `server.js` does is initiate delegate
rendering to `react-router`. At the bottom of `server.js`, we listen to port `3000` and initiate the API server.

#### Routing and HTML return

The primary section of `server.js` generates an HTML page with the contents returned by `react-router`. First we instantiate an `ApiClient`, a facade that both server and client code use to talk to the API server. On the server side, `ApiClient` is given the request object so that it can pass along the session cookie to the API server to maintain session state. We pass this API client facade to the `redux` middleware so that the action creators have access to it.

Then we perform [server-side data fetching](#server-side-data-fetching), wait for the data to be loaded, and render the page with the now-fully-loaded `redux` state.

The last interesting bit of the main routing section of `server.js` is that we swap in the hashed script and css from the `webpack-assets.json` that the Webpack Dev Server – or the Webpack build process on production – has spit out on its last run. You won't have to deal with `webpack-assets.json` manually because [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) take care of that.

We also spit out the `redux` state into a global `window.__data` variable in the webpage to be loaded by the client-side `redux` code.

#### Server-side Data Fetching

We ask `react-router` for a list of all the routes that match the current request and we check to see if any of the matched routes has a static `fetchData()` function. If it does, we pass the redux dispatcher to it and collect the promises returned. Those promises will be resolved when each matching route has loaded its necessary data from the API server.

#### Client Side

The client side entry point is reasonably named `client.js`. All it does is load the routes, initiate `react-router`, rehydrate the redux state from the `window.__data` passed in from the server, and render the page over top of the server-rendered DOM. This makes React enable all its event listeners without having to re-render the DOM.

#### Redux Middleware

The middleware, [`clientMiddleware.js`](https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/redux/middleware/clientMiddleware.js), serves two functions:

1. To allow the action creators access to the client API facade. Remember this is the same on both the client and the server, and cannot simply be `import`ed because it holds the cookie needed to maintain session on server-to-server requests.
2. To allow some actions to pass a "promise generator", a function that takes the API client and returns a promise. Such actions require three action types, the `REQUEST` action that initiates the data loading, and a `SUCCESS` and `FAILURE` action that will be fired depending on the result of the promise. There are other ways to accomplish this, some discussed [here](https://github.com/rackt/redux/issues/99), which you may prefer, but to the author of this example, the middleware way feels cleanest.

#### Redux Modules... *What the Duck*?

The `src/redux/modules` folder contains "modules" to help
isolate concerns within a Redux application (aka [Ducks](https://github.com/erikras/ducks-modular-redux), a Redux Style Proposal that I came up with). I encourage you to read the
[Ducks Docs](https://github.com/erikras/ducks-modular-redux) and provide feedback.

#### API Server

This is where the meat of your server-side application goes. It doesn't have to be implemented in Node or Express at all. This is where you connect to your database and provide authentication and session management. In this example, it's just spitting out some json with the current time stamp.

#### Getting data and actions into components

To understand how the data and action bindings get into the components – there's only one, `InfoBar`, in this example – I'm going to refer to you to the [Redux](https://github.com/gaearon/redux) library. The only innovation I've made is to package the component and its wrapper in the same js file. This is to encapsulate the fact that the component is bound to the `redux` actions and state. The component using `InfoBar` needn't know or care if `InfoBar` uses the `redux` data or not.

#### Images

Now it's possible to render the image both on client and server. Please refer to issue [#39](https://github.com/erikras/react-redux-universal-hot-example/issues/39) for more detail discussion, the usage would be like below (super easy):

```javascript
let logoImage = require('./logo.png');
```

#### Styles

This project uses [local styles](https://medium.com/seek-ui-engineering/the-end-of-global-css-90d2a4a06284) using [css-loader](https://github.com/webpack/css-loader). The way it works is that you import your stylesheet at the top of the class with your React Component, and then you use the classnames returned from that import. Like so:

```javascript
const styles = require('./App.scss');
```

Then you set the `className` of your element to match one of the CSS classes in your SCSS file, and you're good to go!

```jsx
<div className={styles.mySection}> ... </div>
```

#### Unit Tests

The project uses [Mocha](https://mochajs.org/) to run your unit tests, it uses [Karma](http://karma-runner.github.io/0.13/index.html) as the test runner, it enables the feature that you are able to render your tests to the browser (e.g: Firefox, Chrome etc.), which means you are able to use the [Test Utilities](http://facebook.github.io/react/docs/test-utils.html) from Facebook api like `renderIntoDocument()`.

To run the tests in the project, just simply run `npm test` if you have `Chrome` installed, it will be automatically launched as a test service for you.

To keep watching your test suites that you are working on, just set `singleRun: false` in the `karma.conf.js` file. Please be sure set it to `true` if you are running `npm test` on a continuous integration server (travis-ci, etc).

#### How do I disable the dev tools?

They will only show in development, but if you want to disable them even there, set `__DEVTOOLS__` to `false` in `/webpack/dev.config.js`.
67 changes: 67 additions & 0 deletions api/__tests__/api.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {expect} from 'chai';
import {mapUrl} from '../utils/url';

describe('mapUrl', () => {

it('extracts nothing if both params are undefined', () => {
expect(mapUrl(undefined, undefined)).to.deep.equal({
action: null,
params: []
});
});

it('extracts nothing if the url is empty', () => {
const url = '';
const splittedUrlPath = url.split('?')[0].split('/').slice(1);
const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};

expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
action: null,
params: []
});
});

it('extracts nothing if nothing was found', () => {
const url = '/widget/load/?foo=bar';
const splittedUrlPath = url.split('?')[0].split('/').slice(1);
const availableActions = {a: 1, info: {c: 1, load: () => 'baz'}};

expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
action: null,
params: []
});
});
it('extracts the available actions and the params from an relative url string with GET params', () => {

const url = '/widget/load/param1/xzy?foo=bar';
const splittedUrlPath = url.split('?')[0].split('/').slice(1);
const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};

expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
action: availableActions.widget.load,
params: ['param1', 'xzy']
});
});

it('extracts the available actions from an url string without GET params', () => {
const url = '/widget/load/?foo=bar';
const splittedUrlPath = url.split('?')[0].split('/').slice(1);
const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};

expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
action: availableActions.widget.load,
params: ['']
});
});

it('does not find the avaialble action if deeper nesting is required', () => {
const url = '/widget';
const splittedUrlPath = url.split('?')[0].split('/').slice(1);
const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};

expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
action: null,
params: []
});
});
});
5 changes: 5 additions & 0 deletions api/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export loadInfo from './loadInfo';
export loadAuth from './loadAuth';
export login from './login';
export logout from './logout';
export * as widget from './widget/index';
3 changes: 3 additions & 0 deletions api/actions/loadAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function loadAuth(req) {
return Promise.resolve(req.session.user || null);
}
8 changes: 8 additions & 0 deletions api/actions/loadInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function loadInfo() {
return new Promise((resolve) => {
resolve({
message: 'This came from the api server',
time: Date.now()
});
});
}
7 changes: 7 additions & 0 deletions api/actions/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function login(req) {
const user = {
name: req.body.name
};
req.session.user = user;
return Promise.resolve(user);
}
8 changes: 8 additions & 0 deletions api/actions/logout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function logout(req) {
return new Promise((resolve) => {
req.session.destroy(() => {
req.session = null;
return resolve(null);
});
});
}
2 changes: 2 additions & 0 deletions api/actions/widget/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export update from './update';
export load from './load';
28 changes: 28 additions & 0 deletions api/actions/widget/load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const initialWidgets = [
{id: 1, color: 'Red', sprocketCount: 7, owner: 'John'},
{id: 2, color: 'Taupe', sprocketCount: 1, owner: 'George'},
{id: 3, color: 'Green', sprocketCount: 8, owner: 'Ringo'},
{id: 4, color: 'Blue', sprocketCount: 2, owner: 'Paul'}
];

export function getWidgets(req) {
let widgets = req.session.widgets;
if (!widgets) {
widgets = initialWidgets;
req.session.widgets = widgets;
}
return widgets;
}

export default function load(req) {
return new Promise((resolve, reject) => {
// make async call to database
setTimeout(() => {
if (Math.random() < 0.33) {
reject('Widget load fails 33% of the time. You were unlucky.');
} else {
resolve(getWidgets(req));
}
}, 1000); // simulate async load
});
}
Loading